Effective C++(二)构造/析构/赋值运算

  1. 条款05:了解C++默默编写并调用哪些函数

    • 请记住:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。所有这些函数都是public且inline。
    • 唯有当这些函数被需要(被调用),它们才会被编译器创建出来。default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。注意,编译器产生的析构函数是个non-vitual,除非这个class的base class自身声明有virtual析构函数。
    • 至于copy构造函数和copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
    • 只要声明了一个构造函数,编译器就不再为它创建default构造函数。
    • 如果你打算在一个“内涵reference成员”的class内支持赋值操作,你必须自己定义copy assignment操作符。面对“内含const成员”的classed,编译器的反应也一样。
    • 如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classed生成一个copy assignment操作符。
  2. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝

    • 请记住:为驳回编译器自动提供的功能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base classed也是一种做法。
    • 所有编译器产生的函数都是public。如果你不想使用copy构造函数或copy assignment操作符,你可以将copy构造函数或copy assignment操作符声明为private。借由明确声明一个成员函数,你阻止了编译器创建其专属版本;而令其为private,使你成功阻止人们调用它。但是因为member函数和friend还是可以调用private函数。你可以不去定义它们,那么如果某些人不慎调用任何一个,会获得一个连接错误(linkage error)。
    • 将连接错误移至编译器是可能的,可以继承一个专门为了阻止copying动作而设计的base class:
      class Uncopyable{
      protected:
      Uncopyable(){}
      ~Uncopyable(){}
      private:
      Uncopyable(const Uncopyable &);
      Uncopyable & operator=(const Uncopyable &);
      };
      
      继承Uncopyable,编译器尝试着生成一个copy构造函数和copy assignment操作符,而正如条款12所说,这些函数的“编译器生成版”会尝试调用其base class的对应兄弟,那么调用会被编译器拒绝。
  3. 条款07:为多态基类声明virtual析构函数

    • 请记住:
      • polymorphic(带多态性质)的base classed应该声明一个virtual析构函数。如果class带有virtual函数,它就应该拥有一个virtual析构函数
      • Classes的设计目的如果不是作为base classe使用,或不是为了具备多态性质,就不该声明virtual析构函数
    • 当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果过未有定义——实际执行时通常发生的是对象的derived成分没被销毁。于是造成了诡异的“局部销毁”对象。这会可形成资源泄露,败坏之数据结构。解决办法是给base class一个virtual析构函数。
    • 当class不企图被当做base class,令其析构函数为virtual不好。欲实现virtual函数,对象必须携带某些信息,主要用来在运行期间决定哪一个virtual函数该被调用。这份信息由包含一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。所以,只有当class内含至少virtual函数,才为它声明virtual析构函数。
    • 标准string不含任何virtual函数,有些程序员会错误地把它当做base class
    • 相同的分析适用于任何不带virtual析构函数的class,包括所有STL容器如vector,list,set,tr1::unordered_map
    • 有时候令class带一个pure virtual析构函数,可能颇为便利。pure virtual函数可能导致abstract classed——不能被实体化。然而,你必须为这个pure virtual析构函数提供一份定义。
    • 并非所有的base classes的设计目的都是为了多态用途。
  4. 条款08:别让异常逃离析构函数

    • 请记住:
      • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
      • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
    • 如果析构函数抛出异常,那么有两个办法可以避免这个问题:
      • 如果close抛出异常就结束程序,通常通过调用abort完成。abort可以抢先制“不明确行为”于死地。
      • 吞下调用close而发生的异常
    • 如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数
  5. 条款09:绝不在构造和析构过程中调用virtual函数

    • 请记住:在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数那层)
    • 在base class构造期间,virtual函数不是virtual函数。在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期间类型信息(如dynamic_cast和typeid),也会把对象视为base class类型。对象在derived class构造函数开始执行前不会成为一个derived class对象
    • 一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入base class析构函数后对象就成为一个base class对象
    • 确定你的构造函数和析构函数都没有调用virtual函数,而它们调用的所有函数也都服从同一约束。
    • 一种做法是在class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全的调用non-virtual logTransaction。
    • 由于你无法使用virtual函数从base classes向下调用,在构造期间,你可以借由“令derived classes将必要的构造信息向上传递至base class构造函数”替换之而加以弥补。
    • 利用辅助函数创建一个值传给base class构造函数往往比较方便。令此函数为static,也就不可能意外指向“初期成熟之BuyTransaction”对象内尚未初始化的成员变量。所以,“在base class构造和析构期间调用的virtual函数不可下降至derived classes”
  6. 条款10:令operator=返回一个reference to *this

    • 请记住:令赋值操作符返回一个reference to *this
    • 然后这份协议被所有内置类型和标准程序库提供的类型如string, vector, complex, tr1::shared_ptr或即将提供的类型共同蹲守
  7. 条款11:在operator=处理“自我赋值”

    • 请记住:
      • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
      • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一对象时,其行为仍然正确
    • “证同测试”以达到“自我赋值”的检验目的:
      Widget & Widget::operator=(const Widget & rhs)
      {
      if( this == &rhs)   return *this;
      delete pb;
      pb = new Bitmap(*rhs.pb);
      return *this;
      }
      
    • 注意在复制pb所指东西之前别删除pb:
      Widget & Widget::operator=(const Widget & rhs)
      {
      Bitmap * pOrig = pb;
      pb = new Bitmap(*rhs.pb);
      delete pOrig;
      return *this;
      }
      
    • 使用copy and swap技术
      class Widget{
      void swap(Widget &rhs);
      };
      Widget & Widget::operator=(const Widget & rhs)
      {
      Widget temp(rhs);
      swap(temp);
      return *this;
      }
      
      (1)某class的copy assignment操作符可能被声明为“以by value方式接受实参”;(2)以by value方式传递东西会造成一份复件/副本
      Widget & Widget::operator=(Widget rhs)
      {
      swap(rhs);
      return *this;
      }
      
      将"copying动作"从函数本体内移至“函数参数构造阶段”却可编译器有时生成更高效的代码
  8. 条款12:复制对象时勿忘其每一个成分

    • 请记住:
      • Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
      • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
    • 为derived class撰写copying函数,必须很小心地复制器base class成本。你应该让derived class的copying函数调用相应的base class函数
    • 当你编写一个copying函数,请确保:(1)复制所有local成员变量;(2)调用所有base classes内的适当copying函数
    • 你不该令copy assignment操作符调用copy构造函数
    • 构造函数用来初始化新对象,而assignment操作符只施行于已初始化对象身上。
    • 如果你发现你的copy assignment操作符和copy构造函数有相近的代码,消除重复代码的做法是:建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。

猜你喜欢

转载自blog.csdn.net/u010991048/article/details/38277543