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