1.对象的初始化
在某些语境下int x;,x能保证被初始化为0。但是,在其他语境中却无法保证其被初始化为0。例如下例所示:
class Point{ int x, y; }; Point p;
上面的程序中,对象p的成员变量x,y有时候能被初始化为0,有时候则不会。读取未初始化的值会导致不明确的行为。可能会发生如下情况:
a.在某些平台上,仅仅只是读取未初始化的值,可能会让你的程序崩溃。
b.读入一些半随机bits,污染了正在进行读取动作的那个对象,最终导致不可预测的程序行为。
对象的初始化动作何时一定发生,何时不一定发生?
表面上这个问题是一个无法决定的状态,而最佳的处理方法是:永远在使用对象之前先将它初始化。
a.对于无任何成员的内置类型,必须手动完成初始化;
int x = 0; const char* p = "CurryCoder"; double b; std::cin>> b;
b.对于内置类型以外的任何其他东西,初始化任务由构造函数来完成。确保每一个构造函数都将对象的每一个成员初始化。
注意:不要混淆了赋值与初始化操作两个概念,如下例中在构造函数中就对成员变量的一个赋值操作。
#include <iostream> #include <string> #include <list> #include <vector> class PhoneNumber{ }; class ABEntry{ public: ABEntry(); // 默认构造函数声明 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都不是初始化,而是被赋值。初始化的发生时间更早,发生于这些成员变量的默认构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。注意:但是对于内置类型numTimesConsulted来说,不能保证一定在你看到的那个赋值动作的时间点之前获得初始值。
2.列表初始化
ABEntry构造函数一个较好的写法是:使用列表初始化的方式进行。如下例所示:
#include <iostream> #include <string> #include <list> #include <vector> class PhoneNumber{ }; class ABEntry{ public: ABEntry(); // 默认构造函数声明 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){}
上面的程序中,使用列表初始化方式的构造函数比基于赋值方式的那个版本的构造函数效率更高。这是因为基于赋值版本中,首先调用默认构造函数为成员变量设置初值,然后再对它们赋新的值。而在使用列表初始化方式中,针对各个成员变量而设置的实参,被拿去作为各成员变量的构造函数的实参。
对大多数类型来说,单单只调用一次拷贝构造函数比先调用默认构造函数后再调用赋值运行符效率更高。对于内置对象numTimesConsulted,它的初始化和赋值的成本相同,但是为了一致性最好也通过列表初始化方式来初始化。同样道理,在默认构造函数中你也可以使用初始化列表方式。如下所示:
// 将默认构造函数也写成列表初始化方式 ABEntry::ABEntry():theName(), theAddress(), thePhones(), numTimesConsulted(0){}
规定:总是在初始化列表中列出所有成员变量,以免还需要记住哪些成员变量可以无需初值。例如:如果成员初始化列表中遗漏了numTimesConsulted,它就没有初始值,这可能会引起其他问题。
有些情况下(例如:如果成员变量是const或引用,它们就必须使用列表初始化方式,不能赋值),即使成员变量属于内置类型,也必须要用列表初始化方式进行初始化。为了避免需要记住哪些成员变量何时必须用初始化列表方式初始化,何时不需要。最简单的做法是:总是使用列表初始化方式进行初始化。
一个类中往往有多个构造函数,每个构造函数都会有自己的成员初始化列表。如果成员变量在多个构造函数中出现重复。这种情况下,可以合理地在初始化列表中遗漏那些赋值表现像初始化一样好的成员变量,将它们改用为赋值操作,并将那些赋值操作移动到某个函数(通常是private),供所有构造函数调用。这种做法在成员变量的初始值是来自于文件或数据库读入的时候特别有用。
3.成员初始化次序
C++有着十分固定的成员初始化次序,即先初始化基类的成员变量,再初始化派生类的成员变量。类中的成员变量总是以声明次序被初始化。例如上述的ABEntry类中,先初始化theName,再初始化theAddress,再接着是thePhones,最后是numTimesConsulted。即使成员变量在初始化列表中以不同的次序出现,也不会影响初始化的次序。建议:当你在成员初始化列表中列出各个成员变量时,最好总是与成员变量的声明顺序一致。
注意:不同编译单元内定义的non-local static对象的初始化次序
static对象:生命周期是从被构造出来到程序结束运行为止。它主要包括:global对象、定义在namespace作用域内的对象、在class中的对象、在函数中的对象、在file文件作用域内被声明为static的对象。
local static对象:函数内部的对象。non-local static对象:其他static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()函数结束时被调用。
编译单元:产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。
上面的注意中:我们关心的问题涉及至少两个源码文件,每个内含至少一个non-local static对象。问题的本质是:如果某个编译单元的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,而它所用到的这个对象可能还没有被初始化,因为C++对定于与不同编译单元内的non-local static对象的初始化次序没有明确定义。如下例所示:
1#include <iostream> 2#include <string> 3 4/* 不同编译单元内定义的non-local static对象 */ 5 6class FileSystem 7{ 8public: 9 // ..... 10 std::size_t numDisks() const; // 常成员函数的声明 11 // ..... 12}; 13 14// 另一个编译单元 15 16extern FileSystem tfs; 17class Directory 18{ 19public: 20 Directory(const std::string &str); 21 // ... 22}; 23 24Directory::Directory(const std::string &str) 25{ 26 // ... 27 std::size_t disks = tfs.numDisks(); // tfs对象是一个non-local-static对象 28 // ... 29} 30 31int main() 32{ 33 const std::string str = "CurryCoder"; 34 Directory tempDir(str); // 除非tfs在tempDir之前先被初始化,否则tempDir对象的构造函数会用到尚未初始化的tfs对象 35 36 return 0; 37}
现在初始化次序的重要性体现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到还未初始化的tfs。如何能确定tfs会在tempDir之前先被初始化呢?
解决方法:将每个non-local static对象封装到自己的专属函数内(该对象在此函数内部被声明为static),这些函数返回一个引用指向它所含的对象。然后用户调用这些函数,而不直接涉及这些对象。这是因为C++保证函数内的local static对象会在函数被调用期间首次遇上该对象之定义式时被初始化。所以以函数调用替换直接访问non-local static对象。如下例所示:
1/* 解决方法:将每一个non-local-static对象封装到自己专属的函数内(该对象在此函数内部声明为static),这些函数返回一个该对象的引用。然后用户调用这些函数,而不直接调用这些对象 */ 2extern FileSystem tfs; 3class FileSystem 4{ 5public: 6 // ..... 7 std::size_t numDisks() const; // 常成员函数的声明 8 // ..... 9 10 FileSystem &tfs() // 这个函数替换tfs对象,它在类FileSystem中是个static 11 { 12 static FileSystem fs; // 定义并初始化一个local static对象fs 13 return fs; // 返回一个引用指向fs对象 14 } 15}; 16 17// 另一个编译单元 18class Directory 19{ 20public: 21 Directory(const std::string &str); 22 // ... 23 Directory &tempDir() 24 { 25 static Directory td; 26 return td; 27 } 28}; 29 30Directory::Directory(const std::string &str) 31{ 32 // ... 33 std::size_t disks = tfs().numDisks(); 34 // .... 35} 36 37int main() 38{ 39 const std::string str = "CurryCoder"; 40 Directory tempDir(str); 41 42 return 0; 43}
4.总结
(1).为内置对象进行手动初始化,因为C++不保证初始化它们。
(2).构造函数最好使用列表初始化方式,而不要在构造函数内部使用赋值操作。初始化列表中列出的成员变量的次序应该和它们在类中的声明次序一致。(3).为了避免跨编译单元之间初始化次序问题,请以local static对象替换non-local static对象。