Effective C++读书笔记之确定对象被使用前已被初始化

导读:
为内置型对象进行手工初始化,因为c++不保证初始化它们。
构造函数最好使用成员初值列(member initialization list ),而不要在构造函数 本体内使用赋值操作(assignment )。初值列列出的成员变量,其排列次序应该 和它们在class中的声明次序相同。 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local  static对象。

1)永远在使用对象之 前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。
a)  对于无任何成员的内置类型,你必须手工完成此事。例如:
   
   
  1. int x=0; //对int进行手工初始化
  2. const char* text="A C-style string"; //对指针进行手工初始化//(亦见条款3)
  3. double d;
  4. std::cin>>d; //以读取input stream的方式完成初始化
         
b)  内置类型以外的任何其他东西,初始化责任落在构造函数(constructors) 身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。重点介绍, 别混淆了赋值(assignment )和初始化 ( initialization )。考虑一个用来表现通讯簿的class,其构造函数如下:
    
    
  1. class PhoneNumber{ ... };
  2. class ABEntry {
  3. public:
  4. ABEntry(const std::string& name, const std::string& address,
  5. const std::list<PhoneNumber> &phones);
  6. private:
  7. std::string theName;
  8. std::string theAdress;
  9. std::list<PhoneNumber> thePhones;
  10. int numTimersConsulted;
  11. };
  12. ABEntry::ABEntry(const std::string& name, const std::string& address,
  13. const std::list<PhoneNumber>& phones)
  14. {
  15. theName = name;
  16. theAdress = address;
  17. thePhones = phones;
  18. numTimersConsulted = 0;
  19. }
         这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。C++规 定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造 函数内,theName, theAddress和thePhones都不是被初始化,而是被赋值。初始 化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入 ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它 属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
  ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成 员初值列)替换赋值动作:
     
     
  1. ABEntry::ABEntry(const std::string& name, const std::string& address,
  2. const std::list<PhoneNumber>& phones)
  3. :theName(name),
  4. theAdress(address),
  5. thePhones(phones),
  6. numTimersConsulted(0)
  7. { }

    这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版 本(本例第一版本)首先调用default构造函数为theName,theAddres。和thePhones 设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成 员初值列(member initialization list)的做法(本例第二版本)避免了这一问题,因 为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实 参。本例中的 theN~以n~为初值进行copy构造,theAddress以address为 初值进行copy构造,thePhones以phones为初值进行copy构造。
    对大多数类型而言,比起先调用default构造函数然后再调用copy assignment 操作符,单只调用一次‘opy构造函数是比较高效的,有时甚至高效得多。对于内 置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好 也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量, 你都可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可。假设 ABEntry有一个无参数构造函数,我们可将它实现如下:
     
     
  1. ABEntry::ABEntry( )
  2. :theName(),
  3. theAdress(),
  4. thePhones(),
  5. numTimersConsulted(0)
  6. { }

         规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量(如果它们在 初值列中被遗漏的话)可以无需初值。举个例子,由于numTimesConsulted属于内 置类型,如果成员初值列(member initiaiization list)遗漏了它,它就没有初值,因 而可能开启“不明确行为”的潘多拉盒子。     
         有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相 同),也一定得使用初值列。是的,如果成员变量是const或references,它们就 一定需要初值,不能被赋值(见条款5)。为避免需要记住成员变量何时必须在成 员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样 做有时候绝对必要,且又往往比赋值更高效。

          许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这 种classes存在许多成员变量和/或base classes,多份成员初值列的存在就会导致不 受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合
理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋 值操作,并将那些赋值操作移往某个函数(通常是private,供所有构造函数调用。 这种做法在“成员变量的初值系由文件或数据库读入”时特别有用。然而,比起经 由赋值操作完成的“伪初始化”( pseudo-initialization,通过成员初值列(member  initialization list)完成的“真正初始化”通常更加可取。
          C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes 更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明 次序被初始化。回头看看ABEntry,其theN~成员永远最先被初始化,然后是 theAddress,再来是thePhones,最后是numTimesConsulted。为避免可能存在的一些问题, 最好总是以其声明次序为次序。

2) 不同编译单元内定义之non-local static 对象的初始化次序
         所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和 heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内 的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。 函数内的static对象称为local static对象(因为它们对函数而言是local),其他static 对象称为non-local 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对象”的初始化次序并无明确定义。
         C++对“定义于不同的编译单元内的non-local static 对象”的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相 当困难,非常困难,根本无解。在其最常见形式,也就是多个编译单元内的non-local static对象经由“模板隐式具现化,implicit template instantiations”形成(而后者自 己可能也是经由“模板隐式具现化”形成),不但不可能决定正确的初始化次序, 甚至往往不值得寻找“可决定正确次序”的特殊情况。
          幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个 non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接 指涉这些对象。换句话说,non-local static对象被local static对象替换了。 这是Singleton模式的一个常见实现手法 这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调 用期间”“首次遇上该对象之定义式”时被初始化。
     
     
  1. //例子1
  2. extern FileSystem tfs;//预备给客户使用的对象
  3. //使用tfs对象
  4. Directory::Directory( params )
  5. {
  6. ...
  7. std::size_t disks = tfs.numDisks();
  8. ...
  9. }
  10. //使用修改后
  11. FileSystem &tfs()
  12. {
  13. static FileSystem fs;
  14. return fs;
  15. }
  16. //使用fs 文件
  17. Directory::Directory( params )
  18. {
  19. ...
  20. std::size_t disks = tfs().numDisks();
  21. ...
  22. }
         这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一 个local static对象,第二行返回它。这样的单纯性使它们成为绝佳的inlining候选 人,尤其如果它们被频繁调用的话(见条款30)。但是从另一个角度看,这些函 数“内含static对象”的事实使它们在多线程系统中带有不确定性。再说一次,任 何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待 某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段 ( single-threaded startup portion)手工调用所有reference-returning函数,这可消除 与初始化有关的,竞速形势(race conditions )。

总结:
    为避免在对象初始化之前过早地使用它们,你需要做三件事。第一, 手工初始化内置型non-member对象。第二,使用成员初值列(member initialization lists)对付对象的所有成分。最后,在“初始化次序不确定性”(这对不同编译单 元所定义的non-local static对象是一种折磨)氛围下加强你的设计。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值