5.确定对象被使用前已经初始化

何时需要初始化?

一般来说,使用c part of c++中的组件时,声明变量后不初始化就加以使用会得到未知结果,但non-c parts of C++的规则则略有变化。这也就是array不保证其内容被初始化,但vector却有保证的原因。
为了避免得到一些无谓的结果,我们的最佳处理方法就是永远在使用对象前对它进行初始化。


初始化方式

对于内置类型,我们以手工的形式完成初始化。但对于内置类型以外的其他任何东西,初始化交由构造函数来完成。我们的规则也十分简单:确保构造函数会初始化对象的每一个成员变量。关键在于不要混淆赋值和初始化的概念,从而导致效率降低。

自定义类型的赋值与初始化

假定存在class Person,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
public:
Person(const string &name,const string &phoneNumber);
private:
string thename;
string thephoneNumber;
}
//构造函数
Person::Person(const string &name,const string &phoneNumber){
thename = name;//此处并非初始化而是赋值
thephoneNumber = phoneNumber;
}

c++规定,成员变量的初始化动作发生在进入构造函数本体之前。也就是说,刚才thename等变量已被default构造函数初始化过了。但️内置类型除外,因为它不具备default初始化。
因此构造函数的最佳写法是使用member initialization list替换赋值操作,这样会比赋值高效很多。例如:
1
2
Person::Person(const string &name,const string &phoneNumber):
thename(name),thephoneNumber(phoneNumber) {}

这里的成员变量分别以括号内的实参进行了copy或move构造(再次使用了某个构造函数)。

对于内置类型的成员变量,两种写法的效率差不多,但我们为了一致性最好还是统一使用member initialization list中.
甚至当我们只想用default构造函数时,也可以使用此方法,只需要制定nothing作为初始化实参即可,例如:

1
Person::Person():thename(),thephoneNumber() {};

这种写法的高效性不言而喻,但我们必须记住在member initialization list中不允许遗忘任何一个成员变量。虽然非内置类型都具有自己的default构造函数,但是内置类型必须要手动初始化
有时候即使成员变量是内置类型,也一定得使用初值列,比如const或者reference的成员变量必须初始化,且前者无法被赋值。
总之,我们只需要记住,对于自定义类型,我们在构造函数中总是使用member initialization list。


初始化次序

自定义类型的初始化次序

c++有着固定的初始化次序:base class总是先于derived class被初始化,而class中的成员变量总是按照声明的次序初始化。
有可能初值列中的次序不是和声明次序一致,但是初始化的顺序却是始终按声明顺序完成的。因此我们最好严格地按照声明次序来写初值列


“不同编译单元内定义之non-local static对象”的初始化次序

static对象

static对象的寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都不是static。
static对象包括global对象,定义于namespace作用域内的对象、在classes内、以及在file作用域内被声明为static的对象。
函数内的static对象被称为local-static对象(对函数而言是local),其他static对象称为non-local static对象。
程序结束时static对象会被自动销毁,也就是说它们的析构函数会在main()结束时调用。

编译单元

所谓的编译单元,指的是产出单一目标文件的那些源码。基本上它是单一源码文件加上其引入的头文件。

问题描述

我们现在所关心的问题至少涉及两个源码文件,每一个至少包含一个non-local static对象。
这个问题是这样的:某个编译内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象。后者可能在前者之后初始化,这将导致问题的发生。

问题实例

假设有一个FileSystem class,它的作用是让互联网上的文件看起来坐落于本机,由于这个class的作用在于构建一个单一文件系统,因此其产出的对象可能会位于global或namespace作用域内:

1
2
3
4
5
6
7
class FileSystem{
public:
...
size_t numDisks() const;
...
}
extern FileSystem tfs;

假定某些客户建立了一个class以处理FileSystem的目录,那将不可避免地需要使用tfs:
1
2
3
4
5
class Directory{
public:
Directory(parms) {disks=tfs.numDisks();}
...
}

当客户调用该构造函数时,可能tfs尚未被初始化,关键在于如何保证这两个不同源码建立的对象具有初始化的先后次序?

问题解决方案

将每一个non-local static对象搬到自己的专属函数中(在函数内声明为static),这些函数返回一个reference指向它所含的对象。用户调用这些函数,而不是直接指涉对象。换句话说,我们用local static对象替换了non-local static对象。这是singleton模式的一个常见实现。
具体代码如下:

1
2
3
4
5
class FileSystem {...}
FileSystem& tfs(){//具体实操可见More Effective C++ 26
static FileSystem fs;
return fs;
}

这个方法的基础是:函数内的local static对象会在函数被调用期间或者是首次遇到定义式时初始化。所以你在使用函数调用时必然会保证reference指向了一个经历了初始化的对象。而且更棒的是,如果你不调用,则不会触发构造和析构操作。这是真正的non-local static对象所不具备的。

单例模式的纰漏

此操作在多线程下可能会引发问题,处理它的方式是在单线程启动阶段手工调用所有reference-returning函数。


总结

  1. 内置类型必须手工初始化
  2. 使用初值列代替构造函数中的赋值操作。初值列的次序应该与声明次序相同。
  3. 对于跨编译单元之初始化次序问题,请使用local static来代替non-local static。