总结:
1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。
2. 构造函数最好使用成员初值列(memberinitialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
3. 为免除"跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。
关于"将对象初始化"这事,C++ 似乎反复无常。在某些语境下内置类型和类的成员变量保证被初始化,但在其他语境中却不保证。
读取未初始化的值会导致不明确的行为。它可能让你的程序终止运行,可能污染了正在进行读取动作的那个对象,可能导致不可测知的程序行为,以及许多令人不愉快的调试过程。
最佳处理办法就是:永远在使用对象之前先将它初始化。无论是对于内置类型、指针还是读取输入流,你必须手工完成此事:
int x = 0; //int的手动初始化
const char* text = "a C-style string"; //对指针进行手工初始化
double d;
std::in >> d; //以读取方式初始化
内置类型以外的任何其他东西,初始化则由构造函数完成,确保每一个构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,重要的是别混淆了赋值和初始化。考虑一个用来表现通讯簿的class,其构造函数如下:
class PhoneNumber { ... };
class ABEntry { //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 theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name; //这些都是赋值(assignments),
theAddress = address; //而非初始化(initializations)。
thePhones = phones;
numTimesConsulted = 0;
}
这会导致ABEntry对象带有你期望(你指定)的值,但
不是最佳做法。C++ 规定,
对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress和thePhones都
不是被初始化,而是被赋值。初始化的发生时间
更早,发生于这些成员的
default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
使用所谓的 member initialization list(成员初始化列表)替换赋值动作会更好:
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address), //现在,这些都是初始化(initializations)
thePhones(phones),
numTimesConsulted(0)
{ } //现在,构造函数本体不必有任何动作
这个构造函数和上一个的最终结果相同,但
使用构造函数的初始化列表通常效率较高。
基于赋值的那个版本首先调用defalt构造函数为成员变量赋值,然后立刻再对它们赋予新值;default构造函数所做的一切都因此浪费了;本例中成员初始化列表避免了这个问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各个成员变量之构造函数的实参。
对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。对于内置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列。假设ABEntry有一个无参数构造函数,我们可将它实现如下:
ABEntry::ABEntry():theName(), //调用theName的default构造函数;
theAddress(), //为theAddress做类似动作;
thePhones(), //为thePhones做类似动作;
numTimesConsulted(0) //记得将numTimesConsulted显式初始化为0
{ }
当成员是内置类型,一定要使用成员初始化列表,如果成员变量是const或reference,一定需要初值,不能被赋值;为避免需要记住成员变量何时必须在成员初始化列表中被初始化,何时不需要,最简单的做法是:
总是在成员初始化列表中列出所有成员,并总使用成员初值列。
C++ 成员初始化次序base classes早于其derived classes,而class的成员变量总是以其声明次序被初始化(不是在成员列表中出现的次序)。回头看看ABEntry,其theName成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted,即使它们在成员初值列中以不同的次序出现。为避免某些可能存在的晦涩错误(两个成员变量的初始化带有次序性,如初始化array时需要指定大小,因此代表大小的那个成员变量必须先有初值),当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序。
不同编译单元内定义之non-local static对象的初始化次序
static对象:
函数内的static对象称为local static对象,其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
编译单元(translation unit):
产出单一目标文件(single object file)的那些源码,基本上它是单一源码文件加上其所含入的头文件(#include files)。
真正的问题是:如果某编译单元内的某个non-localstatic对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++ 对"定义于不同编译单元内的non-local static对象"的初始化次序并无明确定义。
假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机(local)。由于这个class使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem { //来自你的程序库
public:
...
std::size_t numDisks() const;//众多成员函数之一
...
};
extern FileSystem tfs; //预备给客户使用的对象,tfs代表"the file system"
现在假设某些客户建立了一个class用以处理文件系统内的目录(directories)。很自然他们的class会用上theFileSystem对象:
class Directory { //由程序库客户建立
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks();//使用tfs对象
...
}
进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:
</pre><pre name="code" class="cpp">Directory tempDir( params ); //为临时文件而做出的目录
除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-local static对象。C++ 对"定义于不同的编译单元内的non-localstatic对象"的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。
一个小小设计便可完全消除这个问题:将每个non-localstatic对象搬到自己的专属函数内,并将该对象在此函数内被声明为static,这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是Singleton模式的一个常见实现手法。
C++ 保证,函数内的local static对象会在该函数被调用期间首次遇上该对象之定义式时被初始化。如果你从未调用non-local static对象的"仿真函数",就绝不会引发构造和析构成本!
以此技术施行于tfs和tempDir身上,结果如下:
class FileSystem { ... }; //同前
FileSystem& tfs() //这个函数用来替换tfs对象;它在
{ //FileSystem class中可能是个static。
static FileSystem fs; //定义并初始化一个local static对象,
return fs; //返回一个reference指向上述对象。
}
class Directory { ... }; //同前
Directory::Directory( params )//同前,但原本的reference to tfs
{ //现在改为tfs()
...
std::size_t disks = tfs().numDisks( );
...
}
Directory& tempDir() //这个函数用来替换tempDir对象;
{ //它在Directory class中可能是个static。
static Directory td; //定义并初始化local static对象,
return td; //返回一个reference指向上述对象。
}
这么修改之后,这个系统程序的客户唯一不同的是他们现在使用tfs()和tempDir()而不再是tfs和tempDir,也就是说他们
使用函数返回的"指向static对象"的references,而
不再使用static对象自身。这些函数内含static对象的事实使它们在多线程系统中带有不确定性。