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

总结:

1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。

2. 构造函数最好使用成员初值列(memberinitialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

3. 为免除"跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。


        关于"将对象初始化"这事,C++ 似乎反复无常。在某些语境下内置类型和类的成员变量保证被初始化,但在其他语境中却不保证。

         读取未初始化的值会导致不明确的行为。它可能让你的程序终止运行,可能污染了正在进行读取动作的那个对象,可能导致不可测知的程序行为,以及许多令人不愉快的调试过程

最佳处理办法就是永远在使用对象之前先将它初始化。无论是对于内置类型、指针还是读取输入流,你必须手工完成此事:

int x = 0;   //int的手动初始化
const char* text = "a C-style string";   //对指针进行手工初始化

double d;
std::in >> d;         //以读取方式初始化

内置类型以外的任何其他东西,初始化则由构造函数完成,确保每一个构造函数都将对象的每一个成员初始化

这个规则很容易奉行,重要的是别混淆了赋值初始化。考虑一个用来表现通讯簿的class,其构造函数如下:

class 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::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;    //这些都是赋值(assignments),  
	theAddress = address;  //而非初始化(initializations)。  
	thePhones = phones;  
	numTimesConsulted = 0;  
} 
        这会导致ABEntry对象带有你期望(你指定)的值,但 不是最佳做法。C++ 规定, 对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress和thePhones都 不是被初始化,而是被赋值。初始化的发生时间 更早,发生于这些成员的 default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
            使用所谓的 member initialization list(成员初始化列表替换赋值动作会更好:

ABEntry::ABEntry(const std::string& name, const std::string& address,
	                    const std::list<PhoneNumber>& phones)
	:theName(name),  
	theAddress(address),   //现在,这些都是初始化(initializations)  
	thePhones(phones), 
	numTimesConsulted(0)  
{ }                       //现在,构造函数本体不必有任何动作 
        这个构造函数和上一个的最终结果相同,但 使用构造函数的初始化列表通常效率较高基于赋值的那个版本首先调用defalt构造函数为成员变量赋值,然后立刻再对它们赋予新值;default构造函数所做的一切都因此浪费了;本例中成员初始化列表避免了这个问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各个成员变量之构造函数的实参。

       对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。对于内置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列。假设ABEntry有一个无参数构造函数,我们可将它实现如下:

ABEntry::ABEntry():theName(),  //调用theName的default构造函数;  
	theAddress(),              //为theAddress做类似动作;  
	thePhones(),               //为thePhones做类似动作;  
	numTimesConsulted(0)       //记得将numTimesConsulted显式初始化为0  
{ } 
        当成员是内置类型,一定要使用成员初始化列表,如果成员变量是const或reference,一定需要初值,不能被赋值;为避免需要记住成员变量何时必须在成员初始化列表中被初始化,何时不需要,最简单的做法是: 总是在成员初始化列表中列出所有成员,并总使用成员初值列


        C++ 成员初始化次序base classes早于其derived classes,而class的成员变量总是以其声明次序被初始化(不是在成员列表中出现的次序)。回头看看ABEntry,其theName成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted,即使它们在成员初值列中以不同的次序出现。为避免某些可能存在的晦涩错误(两个成员变量的初始化带有次序性,如初始化array时需要指定大小,因此代表大小的那个成员变量必须先有初值),当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序


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

static对象

函数内的static对象称为local static对象,其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

编译单元(translation unit)

产出单一目标文件(single object file)的那些源码,基本上它是单一源码文件加上其所含入的头文件(#include files)。

真正的问题是

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


假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机(local)。由于这个class使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:

class FileSystem {          //来自你的程序库  
public:  
	...  
	std::size_t numDisks() const;//众多成员函数之一  
	...  
};  
extern FileSystem tfs;  //预备给客户使用的对象,tfs代表"the file system" 
现在假设某些客户建立了一个class用以处理文件系统内的目录(directories)。很自然他们的class会用上theFileSystem对象:
class Directory {               //由程序库客户建立  
public:  
	Directory( params );  
	...  
};  

Directory::Directory( params )  
{  
	...  
	std::size_t disks = tfs.numDisks();//使用tfs对象  
	...  
} 
进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:
</pre><pre name="code" class="cpp">Directory tempDir( params );    //为临时文件而做出的目录 

         除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-local static对象。C++ 对"定义于不同的编译单元内的non-localstatic对象"的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。

         一个小小设计便可完全消除这个问题将每个non-localstatic对象搬到自己的专属函数内,并将该对象在此函数内被声明为static,这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是Singleton模式的一个常见实现手法

        C++ 保证,函数内的local static对象会在该函数被调用期间首次遇上该对象之定义式时被初始化。如果你从未调用non-local static对象的"仿真函数",就绝不会引发构造和析构成本!

以此技术施行于tfs和tempDir身上,结果如下:

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( );  
	...  
}  

Directory& tempDir()        //这个函数用来替换tempDir对象;  
{                           //它在Directory class中可能是个static。  
	static Directory td;    //定义并初始化local static对象,  
	return td;             //返回一个reference指向上述对象。  
} 
        这么修改之后,这个系统程序的客户唯一不同的是他们现在使用tfs()和tempDir()而不再是tfs和tempDir,也就是说他们 使用函数返回的"指向static对象"的references,而 不再使用static对象自身。这些函数内含static对象的事实使它们在多线程系统中带有不确定性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值