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


为什么要确保对象在使用前已经被初始化?因为在对于“将对象初始化”这件事上,C++似乎反复无常。比如

1
int x;

在某些预警下x被初始化(为0),但在其它语境中却不保证。比如:

1
2
3
4
5
class Point{
int x,y;
};

Point p;

p的成员变量有时候被初始化(为0),有时候不会。读取未初始化的值会导致不明确的行为,不可知的行为或者程序终止运行。
最佳处理办法是:永远在使用对象之前先将它初始化。

对于内置类型,必须手工完成此事:

1
2
3
4
5
int x = 0;
const char* text = "A C-Style string";

double d;
std::cin >> d;

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

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

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
public:
Person(const std::string &name, int age);
private:
std::string name;
int age;
};

Person::Person(const std::string &name, int age) {
this->name = name; //这些都是赋值操作 而非初始化
this->age = age;
}

这样创建的Person对象会有预期的值,但不是最佳做法。C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前。所以name并不是初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时。但这对age不为真,因为它属于内置类型,不保证一定在看到的那个赋值动作的时间点之前获得初值。

更好的写法是使用所谓的member initialization list(成员初始化列表)替换赋值动作:

1
Person::Person(const std::string &name, int age) : name(name), age(age) {}

这个构造函数和上一个版本最终结果相同,但效率较高。在第一个版本中,首先调用default构造函数为name设初值,然后
立刻在对它们赋予新值。default构造函数的一切因此浪费了。第二个版本中,参数被用作各个成员变量的构造函数的实参。
本例中,以name为初值,对name进行copy构造。如果不想指定任何参数,也可以:

1
Person::Person() :name(),age(0){}

C++有着十分固定的“成员初始化次序”。是的,次序总是相同: base classes 更早于其 derived classes 被初始
化(见条款 12) ,而 class 的成员变量总是以其声明次序被初始化。所以,为避免错误,在成员初值列中条列各个
成员时,最好总是以其声明次序为次序
。在Person中,name永远是最先被初始化的,然后才是age。

还有一种初始化次序需要考虑:不同编译单元内定义之non-local static对象的初始化次序。

所谓的static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括
global对象,定义于namespace作用域内的对象,在classes内,在函数内以及在file作用域内被声明为static的
对象。函数内的static对象成为local static对象,其它static对象成为non-local static对象。

而编译单元是指产出单一目标文件的源码。基本上它是单一源码文件加上其所含入的头文件。

如果某编译单元内的某一个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对
象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次
序没有明确的定义。为避免这个问题,唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在函
数内被声明为static)。这些函数返回一个reference指向它所包含的对象。换句话说,non-local static对象被local
static对象替换了。这是单例模式的一个常见实现手法。

这个方法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被
初始化。而且,如果从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构的成本,non-local static
对象一定会被创建和析构。

1
2
3
4
static Tool& Tool::getInstance(){
static Tool instance;
return instance;
}

这种实现的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。
虽然这种方法是线程安全的(不会因为多线程的原因创建多个对象),但是也带有不确定性(多线程)。任何一种non-const
static对象,不论是不是local的,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的
单线程启动阶段手动调用所有reference-returning函数,这可消除与初始化有关的“竞速形势”。

请记住:

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初值列,而不要在构造函数内使用赋值操作。初值列表列出的成员变量,其排列次序应该和它们在
    class中声明的次序一致。
  • 为免除”跨编译单元之初始化次序“问题,请以local static对象替换non-local static对象。