Effective C++ 学习笔记(二)构造/析构/赋值运算

参考书籍《Effective C++:改善程序与设计的55个具体做法(第三版)》

5. 了解C++默默编写并调用哪些函数

  • 为什么要?

  • 示例

    • C++自动为类声明的函数:默认构造函数析构函数copy构造函数copy assignment函数
      // 当写下这句时
      class Empty {};
      // 实则是以下内容(当然如果没有用到的函数,编译器就不会编译出来):
      class Empty{
      public:
          Empty() {...}   // 默认构造函数(仅当这个类无任何构造函数时)
          Empty(const Empty& rhs) {...}   // copy构造函数
          ~Empty() {...}  // 析构函数
          Empty()& operator=(cosnt Empty* rhs) {...}  // copy assignment操作符
      };
      
    • 默认的构造函数析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。
    • copy构造函数copy assignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static 成员变量拷贝到目标对象。
      NameObject{
      private:
          std::string nameValue;
          T objectValue;
          ...
      };
      
      NameObject<int> no1("Smallest Prime Number", 12);
      NameObject<int> no2(no1);   //调用生成的copy构造函数
      // 将使用“string的copy构造函数”为类域中nameValue初始化
      // 而内置类型,如这里的int,将拷贝objectValue每一个bits来完成初始化
      
    • 当类域中有引用const对象时,你必须自己定义copy assignment操作符
      class NamedObject{
      private:
          std::string& nameValue;
          const T objectValue;
          ...
      };
      
      NamedObject<int> p(std::string("Persephone"), 2);
      NamedObject<int> s(std::string("Satch", 36));
      
      p = s;  // 出错!
      

    ■ 编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment 操作符,以及析构函数。

6. 若不想使用编译器自动生成的函数,就该明确拒绝

  • 为什么要?

    • 有些情况下,你并不允许copy构造函数和copy assinment操作符的出现
  • 示例

    • 自己自定义声明private并且什么事都没干的copy构造函数和copy assinment操作符
    • 用一个基类,将其copy构造函数和copy assinment操作符声明为private,然后真正不想被拷贝的类继承于这个类。这样做的好处是,真正不想被拷贝的类的成员函数和友元类尝试拷贝时,错误发生在编译期,而上一种做法则是在连接期(毕竟愈早侦测出错误愈好)
      class Uncopyable{
      private:
          Uncopyable(const Uncopyable&);
          Uncopyable& operator=(const Uncopyable&);
          // 不用去实现,参数名称便也不用指定
          ...
      };
      
      class HomeForSale:Uncopyable{
          ...
      };
      

    ■ 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

7. 为多态基类声明virtual析构函数

  • 为什么要?

    • 派生类的析构函数应该交给它自己来做,如果由基类来做,派生类内专属于自己的成员变量就没不会被销毁,这会造成资源泄露问题!
  • 示例

    • 让基类的析构函数变成virtual的

      class TimeKeeper{
      public:
          TimeKeeper();
          virtual ~TimeKeeper();
          ...
      }
      
    • virtual函数的实现原理:每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr(virtual table pointer)所指的那个vtbl(virtual table)——编译器在其中寻找适当的函数指针。

    • 所有 STL 容器都不带 virtual 析构函数,所以绝对不能继承他们!

    ■ polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

    ■ Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。

8. 别让异常逃离析构函数

  • 为什么要?

    • 析构函数吐出异常是危险的,总会带来“过早结束程序”或“发生不明确行为”的风险。看下面例子,当 vector v 被销毁,它有责任销毁其内含的所有 Widget,当在析构第一个元素期间,有个异常被抛出,而第二个Widget析构函数又抛出异常。C++在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。
      class Widget{
      public:
          ~Widget() {...} // 假设这里可能吐出一个异常
          ...
      };
      
      void doSomething() {
          std::vector<Widget> v;
          ...
      }   // v在函数退出时自动销毁
      
  • 示例

    • 如果希望抛出异常就结束程序,通常通过调用abort完成:
      class DBConn{
      public:
          ~DBConn(){
              try {
                  db.close(); // 可能throw出异常
              }
              catch (...) {
                  std::abort();
              }
          }
      private:
          DBConnection db;
          ...
      }
      
    • 吞下异常
      ...
      ~DBConn(){
          try {
              db.close(); // 可能throw出异常
          }
          catch (...) {
              // 做记录工作
          }
      }
      ...
      
    • 如果想让客户有处理异常的机会,可以自己提供一个close函数:
      class DBConn{
      public:
          void close() {
              db.close();
              closed = true;
          }
          ~DBConn(){
              if(!closed) {
                  try {
                      db.close();
                  }
                  catch (...) {
                      // 做记录工作
                  }
              }
          }
      private:
          DBCOnnection db;
          bool closed;
      };
      
      DBConn dbc;
      try {
          dbc.close();
      } catch (...) {
          // 客户可以在此处理异常
      }
      

    ■ 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

    ■ 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

9. 绝不在构造和析构过程中调用virtual函数

  • 为什么要?

    • 在基类构造期间,virtual函数不是virtual函数。当创建一个派生类对象时,会先调用基类的构造函数,而当这个基类的构造函数里调用一个虚函数时 ,这个虚函数会是基类的版本
      class Transaction{
      public:
          Transaction();
          virtual void logTransaction() const = 0;
      };
      Transactoin::Transcation() {
          logTransaction();
          ...
      }
      
      class BuyTransaction: public Transaction {
      public:
          virtual void logTransaction() const;
          ...
      };
      
    • 相同道理也适用于析构函数。一旦派生类析构函数开始执行,对象内的派生类成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入基类析构函数后对象就成为一个基类对象,而C++的任何部分包括virtual函数、dynamic_casts等等也就那么看待它。
  • 示例

    • 在基类里将构造函数里需要调用的virtual函数改为non-virtual,派生类通过参数形式传入必要信息
      class Transaction{
      public:
          Transaction(const std::string& logInfo);
          void logTransaction(const std::string& logInfo) const;  //non-virtual
      };
      Transactoin::Transcation(const std::string& logInfo) {
          logTransaction(logInfo);
          ...
      }
      
      class BuyTransaction: public Transaction {
      public:
          BuyTransaction( [parameters] )
              : Transaction(createLogString( [parameters] )) {
                  ...
          }
      private:
          static std::string createLogString( [parameters] );
          // 必须是static
          ...
      };
      

    ■ 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class (比起当前执行构造函数和析构函数的那层)。

10. 令operator=返回一个 reference to*this

  • 为什么要?

    • 实现连锁赋值
      x = y = z;  // x=(y=z)
      
  • 示例

    • class  Widget{
      public:
          Widget& operator= (const Widget& rhs) {
              return *this;
          }
      };
      

    ■ 令赋值(assignment)操作符返回一个reference to*this。

11. 在operator=中处理“自我赋值”

  • 为什么要?

    • 在自我赋值情况下,可能发生错误的operator版本:
      class Widget {
      public:
          Widget& operator=(const Widget& rhs);
      private:
          Bitmap pb;
          ...
      };
      
      Widget& Widget::operator=(const Widget& rhs) {
          delete pb;
          pb = new Bitmap(*rhs.pb);
          // 如果&rhs==this,那么rhs.pb就已经被delete了!
          return *this;
      }
      
      Widget& Widget::operator=(const Widget& rhs) {
          if(this == &rhs) return *this;
          delete pb;
          pb = new Bitmap(*rhs.pb);
          // 一旦new出异常,pb已被删除,违背异常安全性原则!
          return *this;
      }
      
  • 示例

    • 比较好的做法:

      Widget& Widget::operator=(const Widget& rhs) {
          Bitmap* pOrig = pb;
          pb = new Bitmap(*rhs.pb);
          // 即使&rhs==this,delete前也能new一份拷贝
          delete pOrig;
          return *this;
      }
      
      Widget& Widget::operator=(const Widget& rhs) {
          Widget temp(rhs);   // 函数退出销毁
          swap(temp);         // 完成this和temp数据的交换
          return *this;
      }
      

    ■ 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。

    ■ 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

12. 复制对象时勿忘其每一个成分

  • 为什么要?

  • 示例

    • 当你编写一个copying函数,请确保(1) 复制所有 local 成员变量,(2) 调用所有 base classes 内的适当的copying函数。
    • 如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。

    ■ Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。

    ■ 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。

posted @ 2021-03-01 21:44  ithepug  阅读(87)  评论(0编辑  收藏  举报