EC读书笔记系列之2:条款4 确定对象被使用前已先被初始化
条款4:确定对象被使用前已先被初始化
记住:
★为内置对象进行手工初始化,因为C++不保证初始他们
★构造函数最好使用初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应和它们在class中的声明次序相同
★为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象
-----------------------------------------------------------------------------------------------------------------------------------------------------------
1 区别赋值和初始化
举例:
1 class ABEntry { 2 3 public: 4 ABEntry( const std::string &name, const std::string &address, 5 const std::list<PhoneNumber>& phones 6 ); 7 private: 8 std::string theName; 9 std::string theAddress; 10 std::list<PhoneNumber> thePhones; 11 int numTimesConsulted; 12 }; 13 14 ABEntry::ABEntry( const std::string &name, const std::string &address, 15 const std::list<PhoneNumber>& phones 16 ) { 17 18 theName = name; //这些都是赋值而非初始化 19 theAddress = address; 20 thePhones = phones; 21 numTimesConsulted = 0; 22 }
而C++规定对象的成员变量的初始化发生在进入构造函数本体之前。改进如下:
1 ABEntry::ABEntry( const std::string &name, const std::string &address, 2 const std::list<PhoneNumber>& phones 3 ) 4 :theName(name), //现在这些都是初始化 5 theAddress(address), 6 thePhones(phones), 7 numTimesConsulted(0) 8 { } //构造函数本体无需任何动作
这样效率高的原因:
第一个版本首先需调用default构造函数为theName等设初值,然后再立刻对它们赋新值。default构造函数的一切作为因此浪费了。而版本二初始化列表中的各个实参被拿去作为各成员变量之构造函数的实参。
内置型对象其初始化和赋值成本一样,但为了一致性最好也通过初始化列表来初始化。
当欲要default构造一个成员变量,甚至都可以使用初始化列表,只要指定无物即可,如下:
1 ABEntry::ABEntry( const std::string &name, const std::string &address, 2 const std::list<PhoneNumber>& phones 3 ) 4 :theName( ), //现在这些都空,会调用theName的default构造函数,下同 5 theAddress( ), 6 thePhones( ), 7 numTimesConsulted(0) //记得将内置类型显式初始化为0 8 { } //构造函数本体无需任何动作
有时可以合理地在初始化列表中遗漏那些“赋值表现像初始化一样好”的成员,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数(若有多个构造函数的话)调用。此做法在“成员变量的初值系有文件或数据库读入”时特别有用。
C++成员初始次序很固定:base classes早于derived classes;成员变量以声明次序被初始化。
2 不同编译单元内定义之non-local static对象的初始化次序问题
一点一点来解释:
① 所谓static对象包括(3类):
global对象;
定义于namespace作用域内的对象;
在classes内,函数内,以及在file作用域内被声明为static的对象
注:函数内的static对象成为local static对象,其他static对象称为non-local static对象。
② 所谓编译单元:
产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。
举例:
1 class FileSystem { //来自你的程序库 2 public: 3 ... 4 std::size_t numDisks() const; //众多成员函数之一 5 ... 6 }; 7 extern FileSystem tfs; //预备给客户使用的对象
若用户建立了一个class用来处理文件系统内的目录,很自然其class会用上FileSystem对象:
1 class Directory { //由程序库客户建立 2 public: 3 Directory( params ); 4 ... 5 }; 6 7 Directory::Directory( params ) { 8 ... 9 std::size_t disks = tfs.numDisks(); //使用tfs对象 10 ... 11 }
进一步假设,这些客户决定创建一个Directory类型的对象:
1 Directory tempDir( params );
现在初始化次序的重要性显现出来了:除非tfs先于tempDir被初始化。而C++对“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义!!!
如下设计可消除上面问题:
将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。这个技巧基础在于:c++保证,函数内的local static对象会在:“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以若你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证!!!
改进后程序如下:
1 class FileSystem {...}; //同前 2 FileSystem& tfs() { //用此函数来替换tfs对象。该函数在 3 //FileSystem类中可能是个static。这类函数称为 //reference-returning函数,适合写成inline函数,下同 4 5 static FileSystem fs; //local static对象!!! 6 return fs; 7 } 8 9 class Directory {...}; //同前 10 Directory::Directory( params ) { 11 12 ... 13 std::size_t disks = tfs().numDisks(); //函数来替换对象!!! 14 ... 15 } 16 17 Directory& tempDir() { //此函数用来替换tempDir对象。该函数在 18 //Directory类中可能是个static 19 20 static Directory td; 21 return td; 22 }