0.概述
- 为内置型对象进行手工初始化,因为C++不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作( assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
- 为免除“跨编译单元的初始化次序”问题,以local static对象替换non-local static对象。
1.永远在使用对象之前将其初始化
1.1 内置类型:手动初始化
int x = 0; //对int进行手工初始化
const char* text = "A C-style string"; //对指针进行手工初始化
double d;
std::cin >> d; //以读取input stream的方式完成初始化.
1.2 其他类型:构造函数
规则:确保每一个构造函数都将对象的每一个成员初始化
别混淆赋值和初始化:考虑一个表现通信录的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; // 这些都是赋值
theAddress = address; // 而非初始化
thePhones = phones;
numTimesConsulted = 0;
}
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。
- 在 ABEntry构造函数内,theName、theAddress和 thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
- 但numTimesConsulted属于内置类型,不保证一定在所看到的那个赋值动作的时间点之前获得初值。
ABEntry的较佳写法是使用成员初始化列表替换赋值动作:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
成员初始化列表通常效率更高:
- 基于赋值的版本首先调用默认构造函数为theName、theAddress和 thePhones设初值,然后立刻再对他们赋新值,默认构造函数的一切作为都浪费了;
- 为成员初始化列表中的各个成员变量所设的实参,被拿去作为各个成员变量构造函数的实参:本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。
对于大多数类型,只调用一次copy构造比先调用默认构造函数再调用拷贝赋值操作符要更高效;对于内置类型如numTimesConsulted,初始化和赋值的成本相同,但为了一致性也使用成员初始化列表进行初始化;如果想要默认构造一个成员变量,可以使用成员初始化列,只要指定nothing作为初始化实参即可。
无参数构造函数的实现如下:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(),
theAddress(),
thePhones(),
numTimesConsulted(0)
{}
class含有多个构造函数等原因导致成员初始化列表的重复:可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。
成员初始化次序:
- 基类总是更早于其派生类被初始化
- class的成员变量总是以其声明顺序被初始化(即使在成员初始化列表中以不同的顺序出现,也不会有任何影响)
1.3 不同编译单元内定义的non-local static对象的初始化次序
static对象:寿命从被构造出来直到程序结束为止。这种对象包括global对象、定义于namespace作用域内的对象、在classes 内、在函数内、以及在file作用域内被声明为static的对象。
函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
编译单元:是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。
现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。
问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一-编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
例FileSystem类,可能会产出一个特殊对象,位于global或namespace作用域,象征单一文件系统
class FileSystem { // from your library’s header file
public:
...
std::size_t numDisks() const; // one of many member functions
...
};
extern FileSystem tfs; // 预备给客户使用的对象
假设客户建立了一个class会用上FileSystem对象:
class Directory { // created by library client
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对象,无法确定tfs会在tempDir之前先被初始化。
解决方案:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static).这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是 Singleton模式的一个常见实现手法。
这么做的原因在于:
- C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,就保证了所获得的那个reference将指向一个历经初始化的对象。
- 如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本。
上面的例子可以被写成:
class FileSystem{...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory {...};
Directory::Directory(params)
{
...
std::size_t disks=tfs().numDisks(); //原本的reference to tfs改为tfs()
...
}
Directory& tempDir()
{
static Directory td;
return td;
}
这样以后,用户将使用tfs()和tempDir()而不再是tfs和tempDir。也就是说他们使用函数返回的“指向static对象”的references,而不再使用static对象自身。