Effective C++手册Item 4: 确保 objects(对象)在使用前被初始化

Item 4: 确保 objects(对象)在使用前被初始化

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

C++ 看上去在对象的值的初始化方面变化莫测。例如,如果你这样做,

int x;

在某些情形下,x 会被初始化(为 0),但是在其它情形下,也可能没有。如果你这样做,

class Point {
  int x, y;
};
...
Point p;

p 的 data members(数据成员)有时会被初始化(为 0),但有时没有。如果你从一个不存在 uninitialized objects(未初始化对象)的语言来到 C++,请注意这个问题,因为它非常重要。

读取一个 uninitialized values(未初始化值)会引起 undefined behavior(未定义行为)。在一些平台上,读一个 uninitialized value(未初始化值)会引起程序中止,更可能的情况是得到一个你所读的那个位置上的 semi-random bits(半随机二进制位),最终导致不可预测的程序行为和恼人的调试。

现在,有一些描述关于什么时候能保证 object initialization(对象初始化)会发生什么时候不能保证的规则。不幸的是,这些规则很复杂——我觉得它复杂得无法记住。通常,如果你使用 C++ 的 C 部分(参见 Item 1),而且 initialization(初始化)可能会花费一些运行时间,它就不能保证发生。如果你使用 C++ 的 non-C 部分,事情会有些变化。这就是为什么一个 array(数组)(来自 C++ 的 C 部分)不能确保它的元素被初始化,但是一个 vector(来自 C++ 的 STL 部分)就能够确保。

处理这种事情的表面不确定状态的最好方法就是总是在使用之前初始化你的对象。对于 built-in types(内建类型)的 non-member objects(非成员对象),需要你手动做这件事。例如:

int x = 0;                                // manual initialization of an int
const char * text = "A C-style string";   // manual initialization of a
                                          // pointer (see also Item 3)
double d;                                 // "initialization" by reading from
std::cin >> d;                            // an input stream

除此之外的几乎全部情况,initialization(初始化)的重任就落到了 constructors(构造函数)的身上。这里的规则很简单:确保 all constructors(所有的构造函数)都初始化了 object(对象)中的每一样东西。

这个规则很容易遵守,但重要的是不要把 assignment(赋值)和 initialization(初始化)搞混。考虑下面这个表现一个通讯录条目的 class(类)的 constructor(构造函数):

class PhoneNumber { ... };
class ABEntry {                         // ABEntry = "Address Book Entry"
public:
  ABEntry(const std::string& name, const std::string& address,
          const std::list& phones);
private:
  std::string theName;
  std::string theAddress;
  std::list thePhones;
  int num TimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list& phones)
{
  theName = name;                       // these are all assignments,
  theAddress = address;                 // not initializations
  thePhones = phones;
  numTimesConsulted = 0;
}

这样做虽然使得 ABEntry objects(对象)具有了你所期待的值,但还不是最好的做法。C++ 的规则规定一个 object(对象)的 data members(数据成员)在进入 constructor(构造函数)的函数体之前被初始化。在 ABEntry 的 constructor(构造函数)内,theName,theAddress 和 thePhones 不是 being initialized(被初始化),而是 being assigned(被赋值)。initialization(初始化)发生得更早——在进入 ABEntry 的 constructor(构造函数)的函数体之前,它们的 default constructors(缺省的构造函数)已经被自动调用。但不包括 numTimesConsulted,因为它是一个 built-in type(内建类型)。不能保证它在被赋值之前被初始化。

一个更好的写 ABEntry constructor(构造函数)的方法是用 member initialization list(成员初始化列表)来代替 assignments(赋值):

ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list& phones)
: theName(name),
  theAddress(address),                  // these are now all initializations
  thePhones(phones),
  numTimesConsulted(0)
{}                                      // the ctor body is now empty

这个 constructor(构造函数)的最终结果和前面那个相同,但是通常它有更高的效率。assignment-based(基于赋值)的版本会首先调用 default constructors(缺省构造函数)初始化 theName,theAddress 和 thePhones,然而很快又在 default-constructed(缺省构造)的值之上赋予新值。那些 default constructions(缺省构造函数)所做的工作被浪费了。而 member initialization list(成员初始化列表)的方法避免了这个问题,因为 initialization list(初始化列表)中的 arguments(参数)就可以作为各种 data members(数据成员)的 constructor(构造函数)所使用的 arguments(参数)。在这种情况下,theName 从 name 中 copy-constructed(拷贝构造),theAddress 从 address 中 copy-constructed(拷贝构造),thePhones 从 phones 中 copy-constructed(拷贝构造)。对于大多数类型来说,只调用一次 copy constructor(拷贝构造函数)的效率比先调用一次 default constructor(缺省构造函数)再调用一次 copy assignment operator(拷贝赋值运算符)的效率要高(有时会高很多)。

对于 numTimesConsulted 这样的 built-in type(内建类型)的 objects(对象),initialization(初始化)和 assignment(赋值)没有什么不同,但为了统一性,最好是经由 member initialization(成员初始化)来 initialize(初始化)每一件东西。类似地,当你只想 default-construct(缺省构造)一个 data member(数据成员)时也可以使用 member initialization list(成员初始化列表),只是不必指定 initialization argument(初始化参数)而已。例如,如果 ABEntry 有一个不取得 parameters(参数)的 constructor(构造函数),它可以像这样实现:

ABEntry::ABEntry()
:theName(),                         // call theName's default ctor;
 theAddress(),                      // do the same for theAddress;
 thePhones(),                       // and for thePhones;
 numTimesConsulted(0)               // but explicitly initialize
{}                                  // numTimesConsulted to zero

因为对于那些在 member initialization list(成员初始化列表)中的,没有 initializers(初始化器)的,user-defined types(用户自定义类型)的 data members(数据成员),编译器会自动调用其 default constructors(缺省构造函数),所以一些程序员会认为上面的方法有些过分。这也不难理解,但是一个方针是:在 initialization list(初始化列表)中总是列出每一个 data member(数据成员),这就可以避免一旦发生疏漏就必须回忆起可能是哪一个 data members(数据成员)没有被初始化。例如,因为 numTimesConsulted 是一个 built-in type(内建类型),如果将它从 member initialization list(成员初始化列表)中删除,就为 undefined behavior(未定义行为)打开了方便之门。

有时,即使是 built-in types(内建类型),initialization list(初始化列表)也必须使用。比如,const 或 references(引用)data members(数据成员)是必须 be initialized(被初始化)的,它们不能 be assigned(被赋值)(参见 Item 5)。为了避免记忆什么时候 data members(数据成员)必须在 member initialization list(成员初始化列表)中初始化,而什么时候又是可选的,最简单的方法就是总是使用 initialization list(初始化列表)。它有时是必须的,而且它通常都比 assignments(赋值)更有效率。

很多 classes(类)有多个 constructors(构造函数),而每一个 constructor(构造函数)都有自己的 member initialization list(成员初始化列表)。如果有很多 data members(数据成员)和/或 base classes(基类),成倍增加的 initialization lists(初始化列表)的存在引起令人郁闷的重复(在列表中)和厌烦(在程序员中)。在这种情况下,不能不讲道理地从列表中删除那些 assignment(赋值)和 true initialization(真正的初始化)一样工作的 data members(数据成员)项目,而是将 assignments(赋值)移到一个单独的(当然是 private(私有)的)函数中,以供所有 constructors(构造函数)调用。这个方法对于那些 true initial values(真正的初始值)是从文件中读入或从数据库中检索出来的 data members(数据成员)特别有帮助。然而,通常情况下,true member initialization(真正的成员初始化)(经由一个 initialization list(初始化列表))比经由 assignment(赋值)来进行的 pseudo-initialization(假初始化)更可取。

C++ 并非变幻莫测的方面是一个 object(对象)的数据被初始化的顺序。这个顺序总是相同的:base classes(基类)在 derived classes(派生类)之前被初始化(参见 Item 12),在一个 class(类)内部,data members(数据成员)按照它们被声明的顺序被初始化。例如,在 ABEntry 中,theName 总是首先被初始化,theAddress 是第二个,thePhones 第三,numTimesConsulted 最后。即使它们在 member initialization list(成员初始化列表)中以一种不同的顺序排列(这不幸合法),这依然是成立的。为了避免读者混淆,以及一些模糊不清的行为引起错误的可能性,initialization list(初始化列表)中的 members(成员)的排列顺序应该总是与它们在 class(类)中被声明的顺序保持一致。

一旦处理了 built-in types(内建类型)的 non-member objects(非成员对象)的显式初始化,而且确保你的 constructors(构造函数)使用 member initialization list(成员初始化列表)初始化了它的 base classes(基类)和 data members(数据成员),那就只剩下一件事情需要费心了。那就是——深呼吸先——定义在不同 translation units(转换单元)中的 non-local static objects(非局部静态对象)的 initialization(初始化)的顺序。

让我们一片一片地把这个词组拆开。

一个 static object(静态对象)的生存期是从它创建开始直到程序结束。stack and heap-based objects(基于堆栈的对象)就被排除在外了。包括 global objects(全局对象),objects defined at namespace scope(定义在命名空间范围内的对象),objects declared static inside classes(在类内部声明为静态的对象),objects declared static inside functions(在函数内部声明为静态的对象)和 objects declared static at file scope(在文件范围内被声明为静态的对象)。static objects inside functions(在函数内部的静态对象)以 local static objects(局部静态对象)(因为它局部于函数)为人所知,其它各种 static objects(静态对象)以 non-local static objects(非局部静态对象)为人所知。程序结束时 static objects(静态对象)会自动销毁,也就是当 main 停止执行时会自动调用它们的 destructors(析构函数)。

一个 translation unit(转换单元)是可以形成一个单独的 object file(目标文件)的 source code(源代码)。基本上是一个单独的 source file(源文件),再加上它全部的 #include 文件。

我们关心的问题是这样的:包括至少两个分别编译的 source files(源文件),每一个中都至少包含一个 non-local static object(非局部静态对象)(也就是说,global(全局)的,at namespace scope(命名空间范围)的,static in a class(类内)的或 at file scope(文件范围)的 object(对象))。实际的问题是这样的:如果其中一个 translation unit(转换单元)内的一个 non-local static object(非局部静态对象)的 initialization(初始化)用到另一个 translation unit(转换单元)内的non-local static object(非局部静态对象),它所用到的 object(对象)可能没有被初始化,因为 the relative order of initialization of non-local static objects defined in different translation units is undefined(定义在不同转换单元内的非局部静态对象的初始化的相对顺序是没有定义的)。

一个例子可以帮助我们。假设你有一个 FileSystem class(类),可以使 Internet 上的文件看起来就像在本地。因为你的 class(类)使得世界看起来好像只有一个单独的 file system(文件系统),你可以在 global(全局)或 namespace(命名空间)范围内创建一个专门的 object(对象)来代表这个单独的 file system(文件系统):

class FileSystem {                    // from your library
public:
  ...
  std::size_t numDisks() const;       // one of many member functions
  ...
};
extern FileSystem tfs;                // object for clients to use;
                                      // "tfs" = "the file system"

一个 FileSystem object(对象)绝对是举足轻重的,所以在 theFileSystem object(对象)被创建之前就使用将会损失惨重。

现在假设一些客户为一个 file system(文件系统)中的目录创建了一个 class(类),他们的 class(类)使用了 theFileSystem object(对象):

class Directory {                       // created by library client
public:
   Directory( params );
  ...
};
Directory::Directory( params )
{
  ...
  std::size_t disks = tfs.numDisks();   // use the tfs object
  ...
}

更进一步,假设这个客户决定为临时文件创建一个单独的 Directory object(对象):

Directory tempDir( params ); // directory for temporary files

现在 initialization order(初始化顺序)的重要性变得明显了:除非 tfs 在 tempDir 之前初始化,否则,tempDir 的 constructor(构造函数)就会在 tfs 被初始化之前试图使用它。但是,tfs 和 tempDir 是被不同的人于不同的时间在不同的 source files(源文件)中创建的——它们是定义在不同 translation units(转换单元)中的 non-local static objects(非局部静态对象)。你怎么能确保 tfs 一定会在 tempDir 之前被初始化呢?

你不能。重申一遍,the relative order of initialization of non-local static objects defined in different translation units is undefined(定义在不同转换单元内的非局部静态对象的初始化的相对顺序是没有定义的)。这是有原因的。决定 non-local static objects(非局部静态对象)的“恰当的”初始化顺序是困难的,非常困难,以至于无法完成。在最常见的形式下——多个 translation units(转换单元)和 non-local static objects(非局部静态对象)通过 implicit template instantiations(隐式模板实例化)产生(这本身可能也是经由 implicit template instantiations(隐式模板实例化)引起的)——不仅不可能确定一个正确的 initialization(初始化)顺序,甚至不值得去寻找可能确定正确顺序的特殊情况。

幸运的是,一个小小的设计改变从根本上解决了这个问题。全部要做的就是将每一个 non-local static object(非局部静态对象)移到它自己的函数中,在那里它被声明为 static(静态)。这些函数返回它所包含的 objects(对象)的引用。客户可以调用这些函数来代替直接涉及那些 objects(对象)。换一种说法,就是用 local static objects(局部静态对象)取代 non-local static objects(非局部静态对象)。(aficionados of design patterns(设计模式迷们)会认出这是 Singleton 模式的通用实现)。

这个方法建立在 C++ 保证 local static objects(局部静态对象)的初始化发生在因为调用那个函数而第一次遇到那个 object(对象)的 definition(定义)时候。所以,如果你用调用返回 references to local static objects(局部静态对象的引用)的函数的方法取代直接访问 non-local static objects(非局部静态对象)的方法,你将确保你取回的 references(引用)引向 initialized objects(已初始化的对象)。作为一份额外收获,如果你从不调用这样一个仿效 non-local static object(非局部静态对象)的函数,你就不会付出创建和销毁这个 object(对象)的代价,而一个 true non-local static objects(真正的非局部静态对象)则不会有这样的效果。

以下就是这项技术在 tfs 和 tempDir 上的应用:

class FileSystem { ... };           // as before
FileSystem& tfs()                   // this replaces the tfs object; it could be
{                                   // static in the FileSystem class
  static FileSystem fs;             // define and initialize a local static object
  return fs;                        // return a reference to it
}
class Directory { ... };            // as before
Directory::Directory( params )      // as before, except references to tfs are
{                                   // now to tfs()
  ...
  std::size_t disks = tfs().numDisks();
  ...
}
Directory& tempDir()                // this replaces the tempDir object; it
{                                   // could be static in the Directory class
  static Directory td;              // define/initialize local static object
  return td;                        // return reference to it
}

这个改良系统的客户依然可以按照他们已经习惯的方法编程,只是他们现在应该用 tfs() 和 tempDir() 来代替 tfs 和 tempDir。也就是说,他们应该使用返回 references to objects(对象引用)的函数来代替使用 objects themselves(对象自身)。

按照以下步骤来写 reference-returning functions(返回引用的函数)总是很简单:在第 1 行定义并初始化一个 local static object(局部静态对象),在第 2 行返回它。这样的简单使它们成为 inlining(内联化)的完美的候选者,特别是在它们被频繁调用的时候(参见 Item 30)。在另一方面,这些函数包含 static object(静态对象)的事实使它们在 multithreaded systems(多线程系统)中会出现问题。更进一步,任何种类的 non-const static object(非常量静态对象)—— local(局部)的或 non-local(非局部)的——在 multiple threads(多线程)存在的场合都会发生麻烦。解决这个麻烦的方法之一是在程序的 single-threaded(单线程)的启动部分手动调用所有的 reference-returning functions(返回引用的函数)。以此来避免 initialization-related(与初始化相关)的混乱环境。

当然,用 reference-returning functions(返回引用的函数)来防止 initialization order problems(初始化顺序问题)的想法首先依赖于你的 objects(对象)有一个合理的 initialization order(初始化顺序)。如果你有一个系统,其中 object A 必须在 object B 之前初始化,但是 A 的初始化又依赖于 B 已经被初始化,你将遇到问题,坦白地讲,你遇到大麻烦了。然而,如果你避开了这种病态的境遇,这里描述的方法会很好地为你服务,至少在 single-threaded applications(单线程应用)中是这样。

避免在初始化之前使用 objects(对象),你只需要做三件事。首先,手动初始化 built-in types(内建类型)的 non-member objects(非成员对象)。第二,使用 member initialization lists(成员初始化列表)初始化一个 object(对象)的所有部分。最后,在设计中绕过搞乱定义在分离的 translation units(转换单元)中的 non-local static objects(非局部静态对象)initialization order(初始化顺序)的不确定性。

Things to Remember

  • 手动初始化 built-in type(内建类型)的 objects(对象),因为 C++ 只在某些时候才会自己初始化它们。

  • 在 constructor(构造函数)中,用 member initialization list(成员初始化列表)代替函数体中的 assignment(赋值)。initialization list(初始化列表)中 data members(数据成员)的排列顺序要与它们在 class(类)中被声明的顺序相同。

  • 通过用 local static objects(局部静态对象)代替 non-local static objects(非局部静态对象)来避免跨 translation units(转换单元)的 initialization order problems(初始化顺序问题)。