Effective C++——条款4(第1章)

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

Make sure that objects are initialized before they're used
    关于"将对象初始化"这事,C++似乎反复无常.如果这样写:
int x;
    在某些语境下保证被初始化(为0),但在其他语境下却不保证.
    读取未初始化的值会导致不明确的行为.可能的情况是读入一些"半随机"bits,污染了正在进行读取动作的那个对象.
    最佳的处理办法就是:永远在使用对象之前先将它初始化.对于无任何成员的内置类型,必须手工完成此事.
    至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)上.规则很简单:确保每一个构造函数都将对象的每一个成员初始化.
class PhoneNumbe { ... };
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> thePhone;
    int numTimesConsulted;
};
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都不是被初始化,而是被赋值.
    ABEntry构造函数的一个较佳写法是, 使用所谓的member initializaition list(成员初始化列表)替换赋值操作:
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list<PhoneNumber> &phones)
    : theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0)
{}
    这个构造函数和上一个的最终结果相同,但通常效率较高. 基于赋值的那个版本首先调用default构造函数为theName,theAddress和thePhones 设初值,然后立刻再对它们赋予新值.default构造函数的一切作为都浪费了.成员初始化列表的做法避免了这一问题,因为初始化列表中针对各个成员变量而设的实参,被拿去作为各成员变量的构造函数的实参.本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造.
    对大多数类型而言,比起先调用default构造函数再调用copy assignment操作符,但只调用一次copy构造函数是比较高效的,有时甚至高效得多.
    有些情况下即使面对得 成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定要使用初始化列表. 如果成员变量是 const 或 reference,它们就一定需要初值,不能被赋值.为避免需要记住成员变量何时必须在成员初始化列表中初始化,何时不需要,最简单的做法是:总是使用成员初始化列表.这样有时候绝对必要,且往往比赋值更高效.
     C++有着十分固定的"成员初始化次序".base classes早于其derived classes被初始化,而 class 的成员变量总是以其声明次序被初始化.例如ABEntry,其theName成员永远最先被初始化,然后是theAddress,接着是thePones,最后是numTimesConsulted.即使它们在成员初始化列表中以不同的次序出现,也不会有影响.
    一般已经很小心地将"内置型成员变量"明确地加以初始化,而且也确保构造函数运用成员初始化列表初始化base classes和成员变量,那就只剩下一件事情,就是 "不同编译单元内定义的non-local static对象"的初始化顺序.
    所谓 static 对象,其生命从被构造出来直到程序结束为止,因此 stack 和 heap-based 对象都被排除.这种对象包括global对象,定义于namespace作用域内的对象,在classes内,在函数内,以及在flie作用域内被声明为 static 的对象.函数内的 static 对象称为 local static 对象,其他 static 对象称为non-local static 对象.程序结束时 static 对象被自动销毁,也就是它们的析构函数会在main()结束时被自动调用.
    所谓编译单元(translation unit)是指产出单一目标文件的那些源码.基本上它是单一源码文件加上其所含入的头文件.
     真正的问题是:如果编译单元内的某个non-local static 对象的初始化动作使用了另一编译单元内的某个non-local static 对象,它所用到的这个对象可能尚未被初始化.因为C++对"定义于不同编译单元内的non-local static对象"的初始化次序并无明确定义.
    实例可以帮助理解.假设有一个FileSystem class,它让互联网上的文件看起来好像位于本机.由于这个 class 使世界看起来像个单一文件的系统,可能需要产生出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem {
public:
    std::size_t numDisks() const;
};
extern FileSystem tfs;
    如果客户在theFileSystem对象构造完成之前就使用它,会得到惨重的灾难.
    现在假设某些客户建立一个 class 用以处理文件系统内的目录.很自然的使用FileSystem对象:
class Directory {
public:
    Directory(params);
};
Directory::Directory(params) {
    std::size_t disks = tfs.numDisks();
}
    假设客户决定创建一个Directory对象,用来放置临时文件:
Directory tempDir(params);
    现在,初始化次序的重要性就显现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs.但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立的,它们是定义于不同编译单元内的non-local static 对象.如果能够确定tfs会在tempDir之前先被初始化?
    有一个小小的 设计解决这个问题:将每个non-local static 对象搬到自己的专属函数内(该对象在此函数内声明为static).这些函数返回一个reference指向它所含的对象.然后用户调用这些函数,而不直接指涉这些对象.换句话说,non-local static 对象被local static 对象替换了.这就是Singleton模式的一个常见实现手法.
     这个手法的基础在于:C++保证,函数内的local static 对象会在"该函数被调用期间""首次遇上该对象的定义式"时被初始化.所以如果以"函数调用"(返回一个reference指向local static 对象)替换"直接访问non-local static 对象",就可保证所获得的那个reference将指向一个已经初始化的对象.更棒的是,如果从未调用non-local static 对象的"仿真函数",就绝不会引发构造和析构成本:真正的non-local static 对象没有这个优点.
    以此技术施行于tfs和tempDir上,结果如下:
class FileSystem { ... }
FileSystem &tfs() {                        // 这个函数用来替换tfs对象
    static FileSystem fs;
    return fs;
}
class Directory { ... };
Directory::Directory(params) {            // 之前的tfs改为tfs()
    std::size_t disk = tfs().numDisks();
}
Diretory &tempDir() {                    // 这个函数用来替换tempDir对象
    static Directory td;
    return td;
}
    修改之后,系统程序的客户可以完全像以前使用它,唯一不同的是现在使用tfs()和tempDir()而不再是tfs和tempDir.也就是说使用函数返回的"指向static对象"的references,而不再使用 static 对象本身.
    这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static 对象,第二行返回它.这样的单纯性使它们成为绝佳的inlining候选人.尤其是如果它们被频繁调用的话.但从另一个角度看,这些函数"内含static对象"的事实使它们在多线程系统中带有不确定性.
    运用reference-returning函数防止"初始化顺序问题",前提是其中有着一个对对象而言合理的初始化次序.如果有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化是否能够成功却又受限制于B是否已初始化,这个时候就很麻烦了.因此要避开这种病态的境况,只要这样那么上述的方法就可以提供良好的服务,至少在单线程程序中.
    为避免在对象初始化之前过早地使用它们,需要做三件事情.
    第一,手工初始化内置型non-member对象.
    第二,使用成员初始化列表来初始化所有成员
    第三,在"初始化次序不确定性"氛围下加强设计.
     注意:
     为内置型对象进行手工初始化,因为C++不保证初始化它们.
    构造函数最好使用成员初始化列表,而不要在构造函数内使用赋值操作.初始化列表的成员变量,其排列次序应该和它们在 class 中的声明次序相同.
    为免除"跨编译单元的初始化次序"问题,请以local static 对象替换non-local static 对象.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值