条款04(一):确定对象被使用前已经被初始化

条款04:确定对象被使用前已经被初始化

Make sure that objects are initialized before they’re used.
该条款内容较多,分成两章来进行学习记录。

将对象初始化

关于对象初始化这一事件,C++在不同情境下有不同结果,比如:

int x;

在有些语境下,x会被初始化为0,但有些语境却不是。最直接的例子就是在不同的IDE下,这样的初始化不一致会导致相同的代码产生不同的结果。
再比如:

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

p的成员变量有时会被初始化为0,有时则有不会。这就造成了不一致!

读取未初始化的值是不安全的,因为会导致不明确的行为。有些时候,读取这样的未初始化的值,实际是读取一些“半随机”bits,从而对正在进行的读取动作造成了污染,最终导致了不可知的程序行为。

因此,最佳处理办法就是:永远在使用对象之前将它初始化。
对于无任何成员的内置类型,必须手工完成这样的初始化。
举个例子:

内置类型的初始化

//内置类型
int x = 0;      //对int进行手工初始化
const char* text = "A C_style string.";     //对指针进行手工初始化

double d;
std::cin >> d;      //以读取input stream的方式完成初始化

非内置类型的初始化

而对于内置类型之外的其他类型,初始化由构造函数(constructor)来完成:确保每一个构造函数都讲对象的每一个成员初始化。

但是需要注意的是,赋值(Assignment)初始化(Initialization)的区别要搞清楚。
举个例子:

//表示通讯簿的class
class PhoneNumber { ... };//表示电话号码的类
class ABEntry {     //Address Book Entry,表示通讯簿的class
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;
};

//在类外定义构造函数
//版本1
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
{
    //以下操作全部都是赋值而非初始化
    theName = name;
    theAddress = address;
    thePhones = phones;
    numTimesConsulted = 0;
}

在上面的例子中,C++规定,对象的成员变量的初始化发生在进入构造函数本体之前。因此,在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值

初始化发生的时间更早,发生于这些成员的default构造函数被自动调用时(比进入构造函数本体的时间更早)。

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

//版本2
ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
:theName(name),             //这些操作都是初始化(initialization)
 theAddress(address),       //每一个都调用了copy构造函数
 thePhones(phones),
 numTimesConsulted(0)
{ }                         //因此,构造函数本体就没有其他的动作了

在版本2中的构造函数和版本1的最后结果是相同的,但是效率要更高。

  • 在版本1(基于赋值),首先调用default构造函数为theName,theAddress和thePhones**设初值,然后立即在再对它们赋予新值**。这样看来,default构造函数的所有动作都浪费了。
  • 在版本2(基于成员初值列(member initialization list)),则避免了这个问题。因为初值列中针对各个成员变量而设的实参,被拿去作为各个成员变量的构造函数的实参,依次调用copy构造函数。

在上面的例子中,对于内置类型numTimesConsulted,其初始化成本和赋值的成本基本相同,但是为了一致性,最好也通过成员元初值列来进行初始化。

同样的,当用default构造一个成员变量,可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可:

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

由此,我们有了以下这条规则:

  • 规定总是在初值列中,列出所有的成员变量,以免忘记某些成员变量没有赋值。

例如,由于numTimesConsulted属于内置类型,如果成员初值列遗漏了它,就会造成它没有初值,从而引发可能的“不明确行为”。

总是使用成员初值列

在有的情况下,即使成员变量属于内置类型(初始化成本和赋值成本相同),也一定要使用初值列
即:

  • 当成员变量为const或者reference,它们就一定需要初值不能被赋值
    因此,为了避免需要记住成员变量何时必须在成员初值列中初始化,何时又不需要,最简单的做法:
  • 总是使用成员初值列。
    这样做有时候绝对必要,而且往往比赋值更高效。

赋值得到的“伪初始化”

但是凡事也都有例外。许多classes拥有多个构造函数,而每一个构造函数都有自己的成员初值列。如果这种classes存在许多成员变量和base classes,也也就会导致多份的成员初值列,从而造成大量的重复工作。
在这种情况下:

  • 合理的在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,进而改用它们的赋值操作,并将这些赋值操作移动到某个函数之中(通常是private),以供所这些构造函数调用。

当然了,比起由赋值操作完成的“伪初始化(pseudo-initialization)”,通过成员初值列(member initialization list)完成的“真正初始化”通常更为可取。

猜你喜欢

转载自blog.csdn.net/lym940928/article/details/80865963