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

读取未初始化的值可能会导致不明确的行为。

结论

  • 对于内置类型,必须手工对变量进行初始化。
  • 对于内置类型以外的其它类型,初始化责任落在构造函数中。确保每一个构造函数都将对象的每一个成员初始化。并且最好使用成员初值列初始化
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

topic1: 构造函数初始化成员的两种方式

方案一

class PhoneNumber {...};
class ABEntry 
{
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;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, 
				 const std::list<PhoneNumber>& phones)
{
	theName = name;			// 这些都是复制
	theAddress = address;	// 而非初始化
	thePthones = phones;
	numTimesConsulted = 0;
}

方案二

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构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个那个赋值动作时间点之前获得初值。(最后一句话,没有太懂是什么意思。)

方案比较

两个方案构造函数的最终结果相同,但方案二效率更高。
原因

  • 方案一是基于赋值的版本,首先调用default构造函数为theName,theAddress和thePhones设初值,然后立刻再对他们赋予新值。default构造函数一切作为因此浪费了。
  • 方案二是基于成员值列的做法,方案二避免了对default构造函数的浪费。因为初值列中针对各个成员变量而设的是惨,被拿去作为各成员变量构造函数的实参。本例中的theName以name为初值进行copy构造,theAddress以address为处置进行copy构造,thePhones以phones为初值进行copy构造。
  • 对于大多数类型而言,比起县调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造是比较搞笑的。对于内置类型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好通过成员初值列来初始化。

规则

  • 总是在初值列中列出所有成员变量
  • 总是使用成员值列。因为比赋值更高效。
  • 如果成员变量是const或reference,它们就一定需要初值,不能被赋值。(在class的声明中直接以花括号的形式赋初值)。

topic2: 变量初始化的次序同变量的声明次序有关,和变量顺序不同。

  1. C++有着十分固定的“成员初始化次序”。base class更早于其derived
  2. class被其初始化,而class的成员变量总是以其声明次序被初始化

对于ABEntry,其theName成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted。即使它们在成员初值话列值中以不同的次序出现,也不会有任何影响。
(tips:比如,初始化array时需要制定大小,代表大小的那个成员变量必须先有初值,这个顺序在变量声明的时候确定。)

topic3: non-local static对象初始化次序

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

所谓编译单元(translation unit)是指单一目标文件(single object file)的那些源码。基本上它是单一源码加上其所含入的头文件(#include files)。

我们需要关心的场景如下:

场景一

有两个源码文件,每个源码文件内含至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内声明为static。)

问题:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。

举例
假如你有一个FileSystem class,你可能会产生一个特殊的对象,位于global或namespace作用域内,象征单一文件系统:

class FileSystem	// 来自自己的程序库
{
public:
	...
	std::size_t numDisks() const;	//众多成员函数之一
	...
};
extern FileSystem tfs;		//预备给客户使用的对象;tfs代表“the file system”

note:你的客户如果在theFileSystme对象构造完成前就使用它,会得到惨重的灾情。

现在假设某些客户建立了一个class,用以处理文件系统内的目录。

class Directory				// 由程序库客户建立
{
public:
	Directory(params);
	...
};
Directory::Directory(params)
{
	...
	std::size_t disks = tfs.numDisks(); 	// 使用tfs对象
	...
}

进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:

Directory tempDir( params );	// 为临时文件而做出的目录

现在,初始化次序的重要性凸显出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-local static对象。如何能够确定tfs会在tempDir之前先被初始化?

实际上是无法确定的。C++对于“定义于不同编译单元内的non-local static对象”的初始化相对次序并无明确定义。这是有原因的:决定它们初始化次序相当困难,非常困难,根本无解。在其最常见的形式,也就是多个编译单元内的non-local static对象经由“模版隐式具现化,implicit template instantiations”形成,不单不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。

解决方案

将每一个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。设计模式的迷哥迷妹们想必认出来了,这是单例模式的一个常见写法。

这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以,如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static 对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没这等便宜!

单例模式代码举例

class FileSystem {...};		// 同前
FileSystem& tfs()			// 这个函数用来替换tfs对象;
{							// 它在FileSystem class中可能是个static。
	static FileSystem fs;	// 定义并初始化一个local static对象
	return fs;				// 返回一个reference指向上述对象
}

class Directory	{...};			// 同前
Directory::Directory(params)	// 同前,但原本的reference to tfs
{								// 现改为tfs()
	...
	std::size_t disks = tfs().numDisks(); 	// 使用tfs对象
	...
}
Directory& tempDir()			// 这个函数用来替换tempDir对象
{								// 它在Directory class中可能是个static
	static Directory td;		// 定义并初始化local static对象
	return td;					// 返回一个reference指向上述对象
}

这么修改之后,这个系统程序的客户完全像以前一样用它,唯一不同的是,他们现在使用tfs()和tempDir()而不再是tfs和tempDir。也就是说它们使用函数返回的“指向static对象”的reference,而不再使用static本身。

总结
这些函数“内含static对象”的事实使他们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的“竞速形式”。以上这段话关于non-const static对象多线程场景的阐述,不太懂。

关于“reference-returning函数”防止初始化次序问题,前提是其中又着一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却又受制于B是否已初始化,这个时候就有麻烦了。只要避开如此病态的境况,以上描述的方法应该可以提供良好的服务,至少在单线程程序中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值