Item4: Make sure that objects are initialized before they're used

来源:互联网 发布:vim保存退出命令 mac 编辑:程序博客网 时间:2024/06/04 17:43

于初始化对象的值,C++似乎反复无常。如果你这么写

 

在某些语境下,x会被初始化为0,但其他语境中却不保证。如果你这么写

p的成员变量有时候被初始化为0,有时候不会。

 

     现在,我们终于有一些规则,描述“对象的初始化动作何时一定发生,何时不一定发生”。不幸的是这些规则太复杂,实在难记。

 

     如果你使用C part of C++(条款1),初始化会引发一些运行时成本(incur a runtime cost),那么就不保证会初始化(it is not guaranteed to take place)。一旦进入non-C part of C++,规则就有所变化。这就保证了为什么array(来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)却有此保证。

 

     对付这种无法决定的状态,最好的方法就是每次使用对象之前都进行初始化(always initialize your objects before you use them)。对于内置类型的非成员对象(For non-member objects of built-in types),你需要手工完成这事。例如:

对于内置类型以外的任何其他东西,初始化责任落在了构造函数(constructors)身上。规则很简单:确保每个构造函数初始化对象中的每个成员。

      这个规则很简单,但重要的,不要混淆了初始化(initialization)和赋值(assignment)。考虑一个用来表现通讯录的class:

 

这会导致ABEntry对象带有你期望(你指定)的值。但这不是最佳做法。C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前。在ABEntry构造函数中,theName,theAddress和thePhones都不是初始化,而是赋值。初始化发生的时间更早---先于进入ABEntry构造函数体之前,它们的默认构造函数(default constructors)自动地被调用。但numTimesConsulted并不是这样(this isn't true for numTimesConsulted),因为它是内置类型。不能保证在赋值之前被初始化。

 

      ABEntry构造函数一个较佳的写法是:使用member initialization list(成员初值列)来替换赋值(assignments):

 

这个构造函数与上一个效果相同,但是效率更高。基于赋值的那个版本(第一个版本)首先调用默认构造函数(default constructors)初始化theName,theAddress和thePhones。然后立即再对它们赋予新值。默认构造函数(default constructors)的工作因此就白费了。成员初值列(member initialization list)的做法(本例第二版本)避免了这个问题,因为成员初值列中针对各个成员变量设的实参(argument),被拿去当作各个数据成员的构造函数的实参。在本例中的theName以name的初值拷贝构造(copy construct),thePhones以phones的初值copy构造,theAddress以address的初值copy构造。

 

      对于大多数类型而言,相比于首先调用默认构造函数(default constructor)再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时候甚至高效很多。对于内置类型对象numTimesConsulted,其初始化和赋值的成本相同,但为了一致性起见,也通过成员初值列来初始化。同样道理,甚至当你想要默认构造一个成员变量,你都可以使用成员初值列,只要不指定初始化实参即可(just specify nothing as an initialization argument)。假如ABEntry有一个无参构造函数,我们可以将它实现如下:

 

 

       由于编译器会为用户自定义类型(user-defined types)之成员变量自动默认构造函数(default constructor)---如果那些成员变量在“成员初值列”中没有被指定初值的话。

       但有一个规则,规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量(如果它们在成员初值列中被遗漏的话)可以无需初值。举个例子,由于numTimesConsulted属于内置类型(built-in types),如果成员初值列(member initialization list)遗漏了它,它就没有初值,因而可能开启“不明确行为”的潘朵拉盒子。

 

       有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值成本相同),也一定得使用初值列。是滴,如果成员变量是const或references,它们就一定需要初值,不能被赋值(见条款5)。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对有必要,且又往往比赋值更高效。

 

       许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种classes存在许多成员变量和/或基类(base classes),多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些"赋值表现像初始化一样好"的成员变量。改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在"成员变量的初值系由文件或数据库读入"时特别有用。然而,比起经由赋值操作完成的"伪初始化"(pseudo-initialization),通过成员初值列(member initialization list)完成的“真正初始化”通常更加可取。

 

       C++有关十分固定的“成员初始化次序”。次序总是相同:基类(base classes)更早于其派生类(derived classed)被初始化(条款12)。而class的成员变量总是以其声明次序被初始化。

 

       一旦你已经很小心地将“内置型成员变量”明确地加以初始化,而且也确保你的构造函数运用"成员初值列"初始化基类(base classes)和成员变量,那就只剩唯一一件事情需要操心,那就是-----"不同编译单元内定义之non-local static对象"的初始化次序。

 

       static对象,其寿命从被构造出来直到程序结束为止。因此stack和heap-based对象都不是static对象。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、农业用地file作用域内被声明为static的对象。函数内的static对象称为local static object(因为它们对函数而言是local),其他static对象称为non-local static object。程序结束时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,它让互联网上的文件看起来好像位于本机(local)。由于这个class使世界看起来像一个单一文件系统,你可能会产生一个特殊对象,位于global或namespace作用域内,象征单一文件系统:

 

 

     现在假设某些客户建立了一个class用以处理文件系统内的目录(directories)。很自然他们的class会用上theFileSystem对象:

 

 

      进一步假设,这些客户决定创建一个Directory对象,用来旋转临时文件:

 

 

现在,初始化次序的重要性显现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件中建立起来的,它们是定义于不同编译单元内的non-local  static对象。如何能够确定tfs会在tempDir之前先被初始?

 

       阿欧,你无法确定。再说一次,C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。

 

       幸运的是一个小小的设计就可以完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。

 

       这个小技巧的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个经过初始化的对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没有这么省事!

 

       此技术用于tfs和tempDir身上,结果如下:

 

当然啦,运用reference-returning函数防止“初始化次序问题”,前提是其中有一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前初始化,但A的初始化能否成功又受制于B是否已初始化。这时候你就有麻烦了。坦白说你自作自受。

 

       既然这样,为避免在对象初始化之前过早地使用它们,你需要做三件事。

1、手工初始化内置型non-member对象;

2、使用成员初值例(member initialization lists)对付对象的所有成份;

3、在“初始化次序不确定性”(这对不同编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。

 

请记住

1、为内置型对象进行手工初始化,因为C++不保证初始化它们。

2、构造函数最好使用成员初值列(member initialization list),而不要在构造函数体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中声明的次序相同。

3、为避免“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。

原创粉丝点击