条款04:确定对象被使用前已先被初始化
1️⃣ 问题引入
- 在不同的语境下,对象是否会被初始化时不一定的 !
int x
;- 如果
x
是全局变量,则会被初始化为 0 ; 如果x
是局部变量,则不会被初始化。
- 如果
- 类相关
1
2
3
4
5
6class Point
{
int x , y;
}
···
Point P;- 同理的,
p
的成员变量有时候被初始化为 0,有时候不会。
- 同理的,
- 为什么要初始化对象 ?
- 读取未初始化的值会导致不明确的行为。
- 在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。
2️⃣ 最佳处理方式
- 规则一:永远在使用对象之前就先将它初始化
- 对于内置类型,必须手工完成 ;对于类而言,初始化的责任就落在构造函数将对象的每一个成员都初始化。
- 规则二:总是在成员初值列中列出所有的变量,使得记住还有哪些成员变量。
3️⃣ 别混淆 “赋值” 和 “初始化”
- 案例引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class PhoneNumber {... };
class ABEntry //ABEntry = "Address Book Entry"
{
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>&phones);
private:
std::string theName;
std::stringtheAddress;
std::list<PhoneNumber>thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>&phones)
{
theName = name; //这些都是赋值(assignments),
theAddress = address; //而非初始化(initializations)。
thePhones = phones;
numTimesConsulted = 0;
} - 解析
- 这会导致
ABEntry
对象带有指定的值,但却不是最佳做法。 - 原因
- C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前 。
- 在
ABEntry
构造函数内,theName
,theAddress
和thePhones
都不是被初始化,而是被赋值 。 - 初始化的发生时间更早,发生于这些成员的
default
构造函数被自动调用之时,比进入ABEntry构造函数本体的时间更早 。 - 但这对
numTimesConsulted
不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值 。
- 这会导致
- 最佳写法
- 使用
member initialization list
替换复制动作 。1
2
3
4
5
6ABEntry::ABEntry(const std::string& name, const std::string& address,const std::list<PhoneNumber>&phones)
: theName(name), //现在,这些都是初始化(initializations)
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{} //现在,构造函数本体不必有任何动作 - 构造函数和上一个的最终结果相同,但通常效率较高。
- 原因
- 赋值版本
- 首先调用
default
构造函数为theName
,theAddress
和thePhones
设初值, 然后再调用copy assignment
。
- 首先调用
- 成员初始化版本
- 初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。
- 本例中的
theName
以name
为初值进行copy
构造,theAddress
以address
为初值进行copy
构造,thePhones
以phones
为初值进行copy
构造。
- 赋值版本
- 使用
4️⃣ 成员初始化次序
- 定义
C++
中有着十分固定的 “成员初始化次序” 。
- 次序
base classes
更早于其derived classes
被初始。class
的成员变量总是以其被初始化。
5️⃣ 不同编译单元定义的 “ 非局部静态变量” 的初始化次序
static
对象- 定义
- 所谓
static
对象,其寿命从被构造出来直到程序结束为止,因此stack
和heap-based
对象都被排除。
- 所谓
- 种类
local static
对象 <——> 函数内的static
对象non-localstatic
对象 <——> 非函数内的static
对象。
- 生命周期
- 程序结束时
static
对象会被自动销毁,也就是它们的析构函数会在main()
结束时被自动调用。
- 程序结束时
- 定义
- 编译单元
- 是指产出 ”单一目标文件“ 的 ”源码“ 文件。
关心的问题
涉及两个源码文件,每一个内含至少一个
non-local static
。关键的原因
- 如果某编译单元内的某个
non-local static
对象的初始化动作使用了另一编译单元内的某个non-localstatic
对象,它所用到的这个对象可能尚未被初始化,因为C++
对定义于不同编译单元内的non-localstatic
对象”的初始化次序并无明确定义。
- 如果某编译单元内的某个
案例引入
1
2
3
4
5
6
7
8
9
10class FileSystem
{
public:
···
std::size_t numDisks () const; //众多成员函数之一
···
};
extern FileSystem tfs; //预备给客户使用的对象;
//tfs代表"the file system"FileSystem
对象绝不是一个稀松平常无关痛痒的(trivial
)对象,因此你的客户如果在theFileSystem
对象构造完成前就使用它,会得到惨重的代价。现在假设某些客户建立了一个
class
用以处理文件系统内的目录(directories
)。很自然他们的class
会用上theFileSystem
对象:1
2
3
4
5
6
7
8
9class Directory //由程序库客户建立
{
public:
Directory(params);
};
Directory:: Directory(params);
std::size_t disks = tfs.numDisks(); //使用tfs对象进一步假设,这些客户决定创建一个
Directory
对象,用来放置临时文件1
Directory tempDir(params); //为临时文件而做出的目录
😈😈😈
现在,初始化次序的重要性显现出来了
- 除非
tfs
在tempDir
之前先被初始化,否则tempDir
的构造函数会用到尚未初始化的tfs
。但tfs
和tempDir
是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-localstatic
对象。如何能够确定tfs
会在tempDir
之前先被初始化?
- 除非
原理
- 无法确定,因为
C++
对 “定义于不同的编译单元内的non-localstatic
对象” 的初始化相对次序并无明确定义。 - 这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。
- 在其最常见形式,也就是多个编译单元内的
non-local static
对象经由“模板隐式具现化,implicit template instantiations
”形成(而后者自己可能也是经由“模板隐式具现化”形成),不但不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。
- 无法确定,因为
解决措施
Singleton
模板的实现C++
保证,函数内的local static
对象会在 “该函数被调用期间” “首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference
指向local static
对象)替换“直接访问non-local static
对象”,你就获得了保证,保证你所获得的那个reference
将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static
对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static
对象可没这等便宜!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 GYu的妙妙屋!