文章目录
@Author:CSU张扬
@Email:csuzhangyang@gmail.com or csuzhangyang@qq.com
@我的网站: https://www.cppbug.com
条款04-确定对象被使用前已被初始化
对象的初始化动作何时一定发生,何时不一定发生,这些规则太过复杂。最佳的处理办法就是:永远在使用对象之前将它初始化。
-
对于内置类型,我们必须手动完成初始化。例如:
int x = 0; const char *str = "A C-style String"; double d; std::cin >> d; // 以读取输入流的方式初始化
-
对于其他类型,初始化由构造函数完成,我们要确保每一个构造函数都将对象的每一个成员初始化。
1. 切勿混淆赋值和初始化
例如:
class PhoneNumber { ... };
class ABEntry {
public:
private:
string theName;
list<PhoneNumber> thePhones;
int num;
}
ABEntry::ABEntry(const string& name, const list<PhoneNumber>& phones) {
theName = name; // 这些是赋值,而非初始化
thePhones = phone;
num = 0;
}
对象的成员变量的初始化动作发生在构造函数本体之前(即 {
之前)。所以上例中这些是赋值,而非初始化。例如 theName
它会先调用 string
类的默认构造函数,再对它进行赋新值。
构造函数应该使用成员初值列代替赋值动作,下面的 theName
直接调用 string
类的拷贝构造函数。
ABEntry::ABEntry(const string& name, const list<PhoneNumber>& phones) :
theName(name), thePhones(phone), num(0) { }
- 对于大多数类型而言,先调用默认构造函数再赋值 没有 只调用一次拷贝构造函数高效。
- 对于内置对象而言,初始化和赋值的成本相同。
注意: 如果成员变量是 const
或者是 引用,就更不能用赋值。
2. 内置类型的赋值操作移到函数里,以避免重复的工作
许多类拥有多个构造函数,如果某个类含有许多成员变量和基类,多份成员初值列就会导致重复的工作。在成员初值列中合理的遗漏那些 “赋值和初始化成本相同” 的成员变量,并将这些 赋值操作 移动到某个函数里(通常是 private
),供所有构造函数使用。
在 成员变量的初值由文件或者数据库导入时 特别适用。
3. 成员初始化次序
3.1 类的成员初始化次序
- 基类更早于其派生类初始化。
- 类的成员变量初始化次序和声明次序相同,与成员初值列的次序无关。
例如 array
必须要知道维度大小,所以一定要保证次序。
3.2 不同编译单元内的 non-local static
对象的初始化次序
函数内的静态对象是 local static
,其他静态对象是non-local static
。
假设在两个文件里,每个都至少含有一个 non-local static
对象。若某文件里的 non-local static
对象的初始化使用了另一个文件里的 non-local static
对象,它所用到的这个对象可能未被初始化,因为 C+++ 对于 “定义于不同编译单元内的对象” 的初始化次序没有明确规定。
例如:
-
某个文件系统:
class FileSystem { public: ... ... size_t numDisks() const; ... ... }; extern FileSystem tfs;
-
客户用于处理文件系统的目录
class Directory { public: ... ... Directory( params ) { ... ... size_t disks = tfs.numDisks(); ... ... } }; Directory tempdir( params );
这里的初始化的次序非常重要:除非 tfs
在 tempDir
之前先被初始化,否则 tempDir
的构造函数会用到尚未初始化的 tfs
。我们如何确定 tfs
会在 tempDir
之前被初始化呢?
一个解决办法是:将每个 non-local
对象搬到自己的专属函数内(该对象在此函数内被声明为 static
),函数返回一个引用绑定它所含的对象。然后用户调用这个函数,而不是直接使用其中的对象。(Singleton模式)
C++保证函数内的 local static
对象会在 调用该函数时,首次遇到该表达式时,被初始化。
这样不仅解决了初始化次序问题,并且如果你从未调用这个函数(从未有需求使用这个对象),就绝不会引发构造和析构的成本。真正的 non-local static
对象可不会这样。
我们修改代码:
class FileSystem {
public:
... ...
size_t numDisks() const;
... ...
};
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
class Directory {
public:
... ...
Directory( params ) {
... ...
size_t disks = tfs().numDisks();
... ...
}
};
Directory& tempdir() {
static Directory td;
return td;
}
当然这种 reference-returning
函数可以,避免初始化次序问题,前提是两个对象有着合理的次序。如果对象A必须在对象B前初始化,但A的初始化又受制于B是否已经初始化,这种情况无法解决,应该避免这种情况发生。
4. 总结
- 为内置类型进行手动初始化,因为C++不保证初始化它们。
- 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。成员初值列的成员变量的次序应该和声明次序相同。
- 为了避免 “不同编译单元的初始化次序” 问题,用
reference-returning
函数内的local static
对象替代non-local static
对象。