Effective C++读书笔记(3)

来源:岁月联盟 编辑:exp 时间:2012-02-04

条款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对象。

 
 摘自 pandawuwyj的专栏