读书笔记《Effective C++》条款04:确定对象被使用前已先被初始化

来源:互联网 发布:c语言中字符串的定义 编辑:程序博客网 时间:2024/05/16 09:57

读取未初始化的值会导致不明确的行为。

在C语言中,全局变量和static变量一般会有初始化值,而对于局部变量没有定义初始化值。在C++中,有了class后,情况就更加复杂了。对于何时有初始化值,何时没有,记忆起来很复杂。最佳处理办法:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,必须手动完成。至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。

这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。来看一个例子:

class PhoneNumber {};class ABEntry {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){//以下这些都是赋值(assignments),而非初始化(initializations)。theName = name;theAddress = address;thePhones = phones;numTimesConsulted = 0;}

在class的构造函数函数体内对成员变量是赋值,而非初始化。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。

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

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) :theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0){//现在构造函数本体不必有任何动作}

这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本首先调用default构造函数为theName,theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列的做法避免了这一问题,因此初值列中针对各个成员变量而设的参数,被拿去作为各成员变量之构造函数的实参。本例中的theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。

对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效的多。对于内置类型对象如numTimesConsulted,其初始化和赋值的成本相同,但是为了一致性最好也通过成员初值来初始化。同样,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物作为初始化实参即可。假设ABEntry有一个无参数构造函数,我们可将它实现如下:

ABEntry::ABEntry():theName(),//调用theName的default构造函数theAddress(),//调用theAddress的default构造函数thePhones(),//调用thePhones的default构造函数numTimesConsulted(0)//记得将numTimesConsulted显示初始化为0{}

由于编译器会为用户自定义类型之成员变量自动调用default构造函数——如果那些成员变量在“成员初值列”中没有被指定初值的话。立下一个规则:总是在初值列中列车所有成员变量,以免还得记住哪些成员变量可以无需初值。例如,由于numTimesConsulted属于内置类型,如果成员初值列(member initialization list)遗漏了它,它就没有初值,因而可能造成不明确的行为。

有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列,如果成员变量是const或reference,它们就一定需要初始值,不能被赋值。

为避免需要记住成员变量何时必须在成员初始列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。

许多class拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种class存在许多成员变量和/或base class,多份成员初值列的存在就会导致不受欢迎的重复。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样成本相同”的成员变量,改用它们的赋值操作,并将那些赋值操作符移往某个函数(通常是private),供所有构造函数调用。

C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classes被初始化,而class的成员变量总是以其声明次序被初始化。即使在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。为避免阅读者迷惑,并避免某些可能存在的晦涩错误,当在成员初值列中条列各个成员变量时,最好总是以其声明顺序为次序。

C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。问题点:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化。

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


要点:

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

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

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

0 0