条款04:确定对象被使用前已先被初始化(1)
Make sure that objects are initialized before they're used.
关于"将对象初始化"这事,C++(www.cppentry.com) 似乎反复无常。如果你这么写:
- int x;
在某些语境下x保证被初始化(为0),但在其他语境中却不保证。如果你这么写:
- class Point {
- int x, y;
- };
- ...
- Point p;
p的成员变量有时候被初始化(为0),有时候不会。如果你来自其他语言阵营而那儿并不存在"无初值对象",那么请小心,因为这颇为重要。
读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些"半随机"bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令人不愉快的调试过程。
现在,我们终于有了一些规则,描述"对象的初始化动作何时一定发生,何时不一定发生"。不幸的是这些规则很复杂,我认为对记忆力而言是太繁复了些。
通常如果你使用C part of C++(www.cppentry.com)(见条款1)而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入non-C parts of C++(www.cppentry.com),规则有些变化。这就很好地解释了为什么array(来自C part of C++(www.cppentry.com))不保证其内容被初始化,而vector(来自STL part of C++(www.cppentry.com))却有此保证。
表面上这似乎是个无法决定的状态,而最佳处理办法就是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。例如:
- int x = 0; //对int进行手工初始化
- const char* text = "A C-style string";//对指针进行手工初始化
- //(亦见条款3)
- double d;
- std::cin >> d; //以读取input stream 的方式完成初始化.
至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯簿的class,其构造函数如下:
- class PhoneNumber { ... };
- class ABEntry { //ABEntry = "Address Book Entry"
- public:
- 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; //这些都是赋值(assignments),
- theAddress = address;//而非初始化(initializations)。
- thePhones = phones;
- numTimesConsulted = 0;
- }
这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。C++(www.cppentry.com) 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress和thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:
- ABEntry::ABEntry(const std::string& name, const std::string& address,
- const std::list<PhoneNumber>& phones)
- :theName(name),
- theAddress(address), //现在,这些都是初始化(initializations)
- thePhones(phones),
- numTimesConsulted(0)
- { } //现在,构造函数本体不必有任何动作
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本(本例第一版本)首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列(member initialization list)的做法(本例第二版本)避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。
对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。对于内置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可。假设ABEntry有一个无参数构造函数,我们可将它实现如下:
- ABEntry::ABEntry( )
- :theName(), //调用theName的default构造函数;
- theAddress(), //为theAddress做类似动作;
- thePhones(), //为thePhones做类似动作;
- numTimesConsulted(0)//记得将numTimesConsulted显式初始化为0
- { }
由于编译器会为用户自定义类型(user-defined types)之成员变量自动调用default构造函数--如果那些成员变量在"成员初值列"中没有被指定初值的话,因而引发某些程序员过度夸张地采用以上写法。那是可理解的,但请立下一个规则,规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。举个例子,由于numTimesConsulted属于内置类型,如果成员初值列(member initialization list)遗漏了它,它就没有初值,因而可能开启"不明确行为"的潘多拉盒子。
有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列。是的,如果成员变量是const或 references,它们就一定需要初值,不能被赋值(见条款5)。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。
许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种classes存在许多成员变量和/或base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些"赋值表现像初始化一样好"的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在"成员变量的初值系由文件或数据库读入"时特别有用。然而,比起经由赋值操作完成的"伪初始化"(pseudo-initialization),通过成员初值列(member initialization list)完成的"真正初始化"通常更加可取。