关于对象初始化,c++似乎反复无常,例如:
int x;
在某些语境条件下保证初始化为0,而在另外一些语境下却并没有这个保证。而下边这种情况:
class Point{
int x,y;
};
Point p;
p的成员函数有的时候被初始化(0),有的时候没有被初始化。
现在,已经有了一些规则,对象的初始化何时会发生,何时不会发生。但是这些规则过于复杂,对我们的记忆有一定的挑战,哈哈。
通常如果使用C part of C++而且初始化可能招致运行期成本,那就不保证发生初始化。一旦进入non-C parts of C++,规则有一些变化。这就是为啥array(C part of C++)不保证其内容被初始化,而vector(STL part of C++)却有此保证。
我们的最佳处理状态是,在使用任何对象之前先将它初始化。对于无任何成员的内置类型,必须手动完成此事:
int x = 0; //对int进行初始化;
const char *text = " A C-style string"; //对指针进行初始化;
double d;
std::cin >> d; //采用input stream的方式完成初始化;
至于内置类型以外的其他类型,初始化的责任落在了构造函数身上。规则很简单:确保构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,值得注意的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个表现通讯簿的class:
class PhoneNumber{};
class ABEntry{ //Address Book Entry;
public:
ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones);
private:
std::string theName;
std::string tehAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string &name, const std::string &address, std::list<PhoneNumber> &phones)
{//以下全是赋值(assignment),不是初始化(initialization);
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
ABEntry对象会带给你期望的值,但不是最佳做法。C++规定:对象成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress, thePhone都不是被初始化,而是被赋值。初始化的时间发生的更早,发生于这些成员函数的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为他属于内置类型,不保证在你看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较好写法是,使用所谓的member initializatin list(成员函数初始列)替换赋值动作:
ABEntry::ABEntry(const std::string &name, const std::string &address,
const std::list<PhoneNumber> &phones)
: theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}//构造函数本体不需要任何动作
这个构造函数和上一个的结果相同,但是通常效率较高。基于赋值的那个版本首先调用default构造函数为theName, theAddress,和thePhones设立初值,然后立刻再对他们赋值。default的一切作为此刻都浪费了。成员初始列(member initianlization list)的做法避免了这一问题,因为初始列中针对各个变量设的实参,被拿去作为各个成员函数的实参。本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。
对于大多数类型而言,比起先调用default构造函数再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有事甚至高的多。对于内置类型,如numTimesConsulted,其初始化成本和赋值的成本相同,但为了一致性最好也通过成员初始列来进行初始化。同样道理,如果我们想default构造一个成员变量,也可以使用成员初始列,只要指定nothing作为初始化列就行了,假设ABEntry有一个无参构造构造函数,我们可以实现如下:
ABEntry::ABEntry()
:theName(), //调用theName的构造函数
theAddress(), //调用theAddress的构造函数
thePhones(), //调用thePhomes的构造函数
numTimesConsulted(0) //记得将此内置类型显式定义为0
{}
此处记得一条规则,那就是在成员初始列中列出所有的成员变量,以免还得记住哪些成员变量可以无需初值。举个例子,如numTimesConsulted属于内置类型,如果(member initialization list)遗漏了他,他就没有初值,因而可能开启“不明确行为”的潘多拉盒子。
有些情况下,即使面对的是内置类型,也一定要使用初始列。如果成员变量是const或者reference,他们一定得需要赋初值,而不是赋值。为避免成员变量何时必须在成员初始列中初始化,何时不需要,最简单的做法就是:总是使用成员初始列。
C++有着十分固定的“成员初始化次序”。是的,次序总是相同的:base classes总是先于derived classes被初始化,而class的成员变量总是以其声明次序被初始化。让我们在看一下ABEntry,其theName永远先被初始化,然后是theAddress,之后thePhones,最后是numTimesConsulted,即使他们在member initialization list中以不同的顺序出现,也不会有任何影响。为了避免迷惑,你在member initialization list中条列各个成员时,最好总是以其声明次序为次序。
一旦我们已经 很小心的将内置型成员变量明确的加以初始化,而且也确保构造函数运用member initialization list初始化base classes和成员变量,那就剩下唯一的一件事情需要操心,就是:不同编译单元内定义之non-local static对象的初始化次序。
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap_based对象都被排除。这里所说的static对象包括global对象,定义于namespace对象内的对象,在class内,在函数内,以及在file内被声明为static的对象。函数内的static对象称为local static对象,其他则成为non-local static对象,程序结束时static对象会自动销毁,也就是他们的析构函数会在main()结束时被调用。
所谓编译单元(translation unit)是指产生单一目标文件(single object file)的那些源码。基本上是单一源码文件加上其所包含的头文件。
现在,上面所涉及到的问题至少包括两个源码文件,每一个内含至少一个non-local static对象(也就是说对象是global,或者位于namespace内,或者class内,或者file作用域内被声明为static)。问题是:如果某个编译单元内的某个non-local static对象的初始化动作使用了另外一个编译单元内的non-local static对象,他所使用的这个对象可能没有初始化,因为C++对“不同编译单元内的non-local static 对象”的初始化次序并没有明确定义。
来个例子:
假设有一个FileSystem class,它让互联网上的文件看起来像是本机。由于这个class使世界看起来像个单一文件系统,可能会产生一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;//预备给客户使用的对象,tfs代表"the file system"
FileSystem对象绝不是一个无关痛痒的对象,因此客户如果在theFileSystem对象构造完成前就使用他,会得到惨重的代价:
现在假设建立了一个class用以处理文件系统内部的目录。很自然,他们会用上theFileSystem的对象:
class Directory{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs.numDisks();
}
进一步假设,这些客户决定创建一个directory对象,用来放置临时文件:
Directory tempDir(params);//为临时文件而做出的目录
现在,初始化的重要作用显示出来了,除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但是tfs和tempDir是不同的人在不同的时间创建出来的,他们是定义于不同编译单元内地non-local static对象,如何能确认tfs在tempDir之前先被初始化?
哦,这是无法确认的。
一个小小的设计可以改变这种情形:将每一个non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference用来指向他所包含的对象。然后用户调用这些函数,而不是直接指涉这些对象。换句话就是non-local static对象被local static对象替换了。这个实现的基础在于:C++保证,函数内的non-local static 对象会在函数被调用期间首次遇上该对象之定义式时被初始化。所以如果以函数调用替换直接访问non-local static 对象,就获得了保证,保证所得的那个reference将指向一个经历初始化的对象。 此技术实现于上面的代码如下:
class FileSystem{};//同前
FileSystem &tfs()//这个函数用来替换tfs对象;他在FileSystem中可能是个static。
{
static FileSystem fs;
return fs;
}
class Directory{};
Directory::Directory()
{
std::size_t disks = tfs().numDisks();
}
Directory &tempDir()
{
static Directory td;
return td;
}
修改之后,我们只是使用的是tfs()和tempDir(),而不是tfs和tempDir。也就是使用的是指向static的reference而不是static对象自身。
这样的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回他。当然,他是绝佳的inline候选,特别是经常被调用的话。但是,另一方面,任何一种non-const static对象,不论是local或者non-local,在多线程下“等待某事发生都会有麻烦的。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有的reference-returning函数(??),这可消除与初始化有关的race condition。
为避免对象初始化之前过早的使用他们,你需要做的是三件事:手工初始化内置的non-member对象;使用member initialization list;在初始化次序不确定下加强你的设计。
Things to remember:
1.Manually initialize objects of built-in type, because C++only sometimes initializes them itself;
2.In a constructor, prefer to use the member initialization list to assigenment inside the body of the constructor. List data member in the initialization list in the same order they're declared in the class.
3.Avoid initialization order problems across translation units by replacing non-local static objects with local static objects.
开始写effective c++的读书笔记。今天是条款2:尽量以const,enum,inline替换#define(prefer consts,enums,and inlines to #define.)
现在在维护代码的时候,前辈们大片大片的宏搞得我是那个晕头转向啊,真希望他们也看过本条款
。
1.Case:#define ASPECT_RATIO 1.653
Recommendation:const double AspectRatio = 1.653;
Reason: 当使用ASPECT_RATIO但是获得一个编译错误信息时,可能你会很是发冏,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。如果ASPECT_RATIO定义在非你所写的头文件中,你更是因为追踪他而浪费时间。改为推荐的方式后,你找到的肯定是AspectRatio。当以常量替换#define时,有两种注意的情况,第一种是定义常量指针(const pointers)。由于常量定义式常放在头文件内,因此有必要将指针也声明为const。例如在一个头文件内定义一个常量的char*-based字符串,必须写const两次:
const char* const authorName = "Edmund";
这里采用string对象比其前辈char*-based更合适,
const std::string authorName("Edmund");
第二种是class专属常量。为了将常量的作用域限制在class内,你必须让他成为class的一个成员;而为确保此常量只有一个实体,则必须声明为static:
class GamePlayer{
private:
static const int NumTurns = 5;
int scores[NumTurns];
...
}
然而,你看到的是NumTurns的声明式而不是定义式,C++通常要求我们所使用的任何东西都要有一个定义式,但如果他是个class的专属常量而又是static且为整数类型(ints,chars,bools),则做特殊处理。只要不取他们的地址,你可以声明并使用他们而无需提供定义式。但如果取某个class专属常量的地址,或纵使不取地址而编译器却坚持要看到一个定义式,你就必须提供另外一个定义式:
const int GamePlayer::NumTurns;
由于NumTurns在声明时已经获得了初值,因此定义时不可以再设初值。此外,对所谓的“in-class初值设定”也只允许对整数常量进行。如果为非整型则可以采用下面的这种方式:
class CostEstimate{
private:
static const double FudgeFactor;
...
}
const double CostEstimate::FudgeFactor = 1.35;
当你在编译期需要一个class常量值,例如在上述GamePlayer::scores的数组声明中,此时如果编译器不允许“static整数型class常量”完成“in-class初值设定”,可采用enum来解决,其理论基础是“一个属于枚举类型的数值可权充ints被使用”,于是GamePlayer可定义如下:
class GamePlayer{
private:
enum{NumTurns = 5};
int scores[NumTurns];
...
};
注意:取一个const的值是合法的,但是取一个enum的值就是不合法的,取一个#define的值也是不合法的。如果你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮助你实现这个约束。
下边继续说预处理器。另外一个常见的#define误用的情景是以他来实现宏,宏看起来像函数,但是不会招致函数调用带来的额外开销,例如:
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))。他的缺点就不说了,替代方式:
template<class T> inline void callWithMax(const T& a, const T& b)
{
f(a > b?a : b);
}
本条目总结:
1.对于单纯常量,最好以const对象或者enums替换#defines;
2.对于形似函数的宏,最好改用inline函数替换#defines。
Ps:本文是第一次在cppblog上发表的文章,呵呵。很早就想在这上面写点了,但是不是忙这就是忙那,昨天下定决心,先把effective C++(3e)里面的55条读书笔记写在这上面。打算每天一个条目,这里面好多跟书上的句子一样,但是全是我自己敲进去的,不存在任何的paste。所写均是自己搞清楚的,不明白地方的暂时没有添加。