条款04:确定对象被使用前已经被初始化
Make sure that objects are initialized before they’re used.
成员初始化次序
在C++中,有十分固定的“成员初始化次序”:
- base classes 早于其derived classes被初始化;
- class的成员变量按照其声明顺序被初始化。
即使成员变量在成员初值列以不同的次序出现(不合法的做法),也不会有影响。
为了避免疑惑,成员初值列中的各个成员的次序,应该按照声明的次序来进行排列。
non-local static对象
除了以上的初始化,还需要考虑“不同编译单元内定义的non-local static对象”的初始化次序。
static
所谓的static对象,其寿命从被构造出来直到程序结束为止,因此,stack和heap-based对象都被排除了。这种对象包括:global对象、定义在namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明的static对象。
在函数内的static对象称为local static对象(因为它们对函数而言是local),而其他的static对象称为non-local static对象。
在程序结束时,static对象会被自动销毁,也就是说,它们的析构函数会在main()结束时自动调用。编译单元
所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。
基本上,就是指单一源码文件加上其所包含的头文件(#include files)
因此,这里所涉及到的问题至少为两个源码文件,每个文件内至少包含一个non-local static对象(即,该对象是global或者位于namespace作用域内,或在class内或file作用域内被声明为static)。
真正的问题为:
- 如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。
因此,C++对于“定义于不同编译单元内的non-local static对象”的初始化次序并没有明确定义。
举个例子:
假如现在有一个FileSystem class,它的作用是让互联网上的文件看起来好像是位于本机的。由于这样的特性,使得整个互联网文件系统看起来像是一个单一的文件系统,此时,可能会出现一个特殊对象,位于global或namespace作用域内,象征着单一文件系统:
class FileSystem {
public:
...
std::size_t numDisks() const;//众多成员函数之一
...
};
extern FileSystem tfs;//预备给客户使用的对象,tfs代表“the file system”
在上面定义的对象中,如果客户在theFileSystem对象构造完成之前就去使用,将会出现严重的错误!
接下来,假设客户建立了一个class用以处理文件系统内的目录(directories),则必然会用到theFileSystem对象:
class Directory {
public:
Directory( params ):
...
};
Directory :: Directory( params )
{
...
std::size_t disks = tfs.numDisks();//使用了tfs对象
...
}
更进一步,用于决定创建一个Directory对象,用来放置临时文件:
Directory tempDir( params );//为临时文件而做出的目录
于是,初始化次序的重要性就体现出来了:
- 除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。
- 但是tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义在不同编译单元内的non-local static对象。
然而,C++对“定义在不同的编译单元内的non-local static对象”的初始化相对次序并没有明确的定义,因此,无法确定tfs会在tempDir之前被初始化。
解决思路:
- 将每个non-local static对象放到自己的专属函数内(该对象在此函数内被声明为static)。
- 这些函数返回一个reference指向它所含的对象。
- 之后,用户调用这些函数,而不是直接涉及这些对象。
- 即,non-local static对象被local static对象替换了。
这个解决思路的基础在于:
C++保证,函数内的local static对象会在“该函数被调用期间、首次遇上该对象的定义式时”被初始化。
当我们以函数调用(返回一个reference指向local static对象)替换“直接访问non-local static对象”时,就获得了C++的这个保证——即获得的那个reference将指向一个已经经历初试化的对象。
另外,如果从未调用该non-local static对象的“仿真函数”,就不会引发构造和析构的成本!
用这种方法改良上面的Directory class:
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指向上述的对象。
}
在经过了这样的修改之后,此时所使用的是函数返回的“指向static对象”的references,而不再使用static对象自身。
利用这种reference-returning函数防止“初始化次序问题”,前提是有这一个对对象而言合理的初始化次序。
为了避免在对象初始化之前而过早的使用它们:
- 第一,手工初始化内置型non-member对象。
- 第二,使用成员初值列(member initialization lists)对付对象的所有成分。
- 第三,在“初始化次序不确定性”环境下,加强程序逻辑的设计。
最后: