[笔记]Effective C++ 条款4:确定对象被使用前已被初始化

本文详细阐述了C++编程中初始化内置类型的重要性,强调了构造函数使用成员初始化列表而非赋值操作的优越性,以及如何通过局部静态对象解决跨编译单元初始化次序的问题。通过示例解释了如何避免不明确的行为,提高代码的稳定性和可维护性。
摘要由CSDN通过智能技术生成

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

对于无任何成员的内置类型,必须手动完成初始化。

int x = 0;	        //对int进行手工初始化
const char* text = "A C-style string";	//对指针进行手工初始化
double d;
std::cin >> d;	    //以读取input stream的方式完成初始化

对于内置类型以外的任何其他东西,则在构造函数中确保每一个成员函数初始化。

区别赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯录的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;
    numTimeConsulted = 0;
}

ABEntry构造函数的一个较好的写法是,使用所谓的member initialization list(成员变量数值列)替换赋值动作:

ABEntry::ABEntry(const std::string& name, \
                const std::string& address, \
                const std::list<PhoneNumber>& phones)
    :theName(name),
     theAddress(address),	//现在,这些都是初始化(initialization)
     thePhones(phones),
     numTimeConsulted(0)
{ }				            //现在构造函数本体不必有任何动作

ABEntry无参数构造函数,可实现如下:

ABEntry::ABEntry()
    :theName(),	            //调用theName的default构造函数
     theAddress(),
     thePhones(),
     numTimesConsulted(0)	//将numTimesConsulted显式初始化为0
{ }

总是在初值列中列出所有成员变量,以免忘记哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。numTimesConsulted属于内置类型,如果初值列(member initialization list)遗漏了它,它就没有初值,因而可能开启“不明确行为”的潘多拉盒子。

有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列。如果成员变量是const或reference,它们就一定需要初值,不能被赋值。

最简单的做法就是:总是使用成员初值列。

classes拥有多个构造函数的话每个构造函数都有自己的成员初值列。如果classes存在许多成员变量或base classes,多份初值列会导致许多重复工作。这种请况下可以合理的在初值列中遗漏那些“赋值变现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个操作函数(通常是private),供所有构造函数调用。这种做法在“成员变量的初值系有文件或数据库读入”时特别有用。

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

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

static对象,其寿命为从被构造出来到程序结束为止。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。

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

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

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

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

某些客户建立了一个class用以处理文件系统内的目录(directories),会在class中用到FileSystem对象。

如果FileSystem对象在构造完成前就使用它,则会出现很坏的情况。

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

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

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

这时候初始化次序变得十分重要了。但你无法确定它们的初始化次序。

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

这个手法的基础在于:C++保证,函数内的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();
    ...
}
Directory& tempDir()		    //这个函数用来替换tempDir对象;
{                               //它在Directory class中可能是个static。
    static Directory td;		//定义并初始化local static对象,
    return td;			        //返回一个reference指向上述对象。
}
  1. 为内置型对象进行手动初始化,因为C++不保证初始化它们。
  2. 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们class中的声明次序相同。
  3. 为免除“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值