effective c++ (三)

条款07:为多态基类申明virtual析构函数

 1、c++明白指出,当derived class对象经由一个base class指针被删除,而该base class带有一个non-virtual 析构函数,其结果未有定义----实际执行时通常发生的是对象derived成分没有被销毁,于是造成一个诡异的局部销毁对象。(子类的析构函数不会被调用)::消除这个问题的做法很简单,给base class一个virtual析构函数,此后删除derived class对象就如你所想要的那般全部销毁。

2、对于不企图当作base class来使用的类,将其析构函数指定未virtual往往是一个馊主意。

classs Point
{
public:
      Point(int xCoord, int yCoord);
      ~Point();
private:
     int x, y;
}   

如上面例子,当析构函数为non-virtual函数时,对象所占空间为64bit(2*int),而当析构函数为virtual函数时,由于对象中会新增virtual table指针,故Point所占空间为96bit(32位系统)

3、pure virtual函数的使用,存在纯虚函数的类位抽象类,无法对其进行实例化(多态实现的一种方式)

请记住:

  • ploymorphic带多态性质的base class应该声明一个virtual析构函数,如果class带有任何virtual函数,他就应该拥有一个virtual析构函数。
  • Class的设计母的不是作为base class使用,或不是为了具备多态性(ploymorphically)就不应该声明virtual析构函数

条款08:别让异常逃离析构函数

1、c++并不禁止析构函数吐出异常,但它不鼓励你这样做,这样是有理由的。

Class Widget
{
public:
     ....
     ~Widget() { ... }  //Suppose an exception to throw out possibly
} 

void doSomething()
{
     std::vector<Widget> v;
     ...
}  //v在这里将被自动销毁

如上代码,当vector v被销毁,它将负责销毁其内含的所有Widget,假设v内含有10个Widget,而在析构第一个元素期间,有异常被抛出,但其他9个widget还是应该被销毁(否则它们报的任何资源都会发生泄漏),因此应该调用v的各个析构函数。但是若在那些调用期间,第二个widget析构函数又抛出异常,现在两个同时作用的异常,这对C++而言太多了,当两个异常同时存在的情况下,程序不是结束执行就是导致不明确行为。本例中会导致不明确行为。

2、如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,怎么处理?

//假设你使用一个class负责数据库连接
class DBConnection
{
public:
     ....
     static DBConnection create();
     
      void close();
}

为了确保客户不忘记在DBConnection对象上调用close,一个合理的想法是创建一个用来管理DBConnection资源的class,并在其析构函数中调用close。

class DBConn   //这个class用来管理DBConnection对象
{
public:
       ...
       ~DBConn() //确保数据库连接总是会被关闭
       {
              db.close();
       }
private:
       DBConnection db;
}

//这便允许客户写出这样的代码
{
      DBConn dbc(DBConnection::create());//建立一个dbc,通过dbc使用db
      ....
}  //在dbc作用域结束时,DBConnection会随这DBConn的析构而关闭

但时上面代码中,如果调用close顺利,不出现任何异常,那将一切美好。但是若调用出现异常,DBConn析构函数会传播异常,也就是允许程序离开这个析构函数,那就会造成问题,因为抛出了难以驾驭的麻烦。

两个办法可以解决这个问题,DBConn的析构函数可以:

  • 如果close抛出异常就结束程序,通常通过调用abort完成
    DBConn::~DBconn()
    { 
         try { db.close(); }
         catch( ... ){
         //制作运转记录,记下对close的调用失败
         std::abort();
          }
    } 

    如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理的选项。毕竟他可以阻止异常从析构函数传播出去(那会导致不明确行为),也就是说调用abort可以抢先置“不明确行为”于死地。

  • 吞下因调用close而发生的异常
    DBConn::~DBconn()
    { 
         try { db.close(); }
         catch( ... ){
         //制作运转记录,记下对close的调用失败
          }
    } 

    一般而言,将异常吞掉是一个坏主意,因为它压制了“某些动作失败”的重要信息,然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。

这些办法都没有什么吸引力,问题在于两者都无法对“导致close抛出异常”的情况做出反应;一个较佳的策略是重新设计DBConn接口,使客户有机会对可能出现的问题作出反应

class DBConn
{
public:
      ...
      void close()     //供客户使用的函数
      {
        db.close();
        closed = true;
      }
     ~DBConn()
     {
         if(!closed)   //关闭连接,如果客户不那么做的话
         {
              try{
                    db.close();
               }catch( ... ){
                      //制作运行记录,记下对close的调用失败
                      ...    /吞下异常或记录并结束程序
               }
         }
     }
private:
     DBConnection db;
     bool closed;
}

此类将在析构函数中可能抛出的异常转移到普通函数中,使得客户获得处理异常得机会。此处即使客户不关心或不处理,而DBConn的析构函数将会完成相应的工作,从而形成双保险

请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,则析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序
  • 如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:绝不在构造和析构过程中调用virtual函数

1、base class构造期间virtual函数绝不会下降到derived classes阶层;由于base class构造函数的执行更早于derived class构造函数,当base构造函数执行时derived的成员变量尚未初始化。

Class Transaction{   //所有交易的base class
public:
       Transaction();
       virtual void logTransaction () const = 0; //做出一份因类型不同而不同的日志记录
       ...
};

Transaction::Transaction()
{
       ....
       logTransaction();   //最后动作标志这笔交易
}

class BuyTransaction: public  Transaction {
public:
      virtual void logTransaction() const; //记录日志
       ...
};


class SellTransaction: public  Transaction {
public:
      virtual void logTransaction() const; //记录日志
       ...
};

如代码中,derived class(BuyTransaction)的virtual logTransaction的将在BuyTransaction被实例化之前调用,从而导致编译器找不到logTransaction的定义。

2、此原则对于析构函数也是类似的。类的析构函数一旦开始调用,程序就会认为该对象已经不存在,不能够使用其内的成员。

3、另外注意被non-virtual函数封装后的virtual函数在构造函数或析构函数中的调用,也是需要禁止的。

请记住:

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(相对于当前执行构造函数和析构函数的那层)
原文地址:https://www.cnblogs.com/penghuster/p/6260511.html