• 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(相对于当前执行构造函数和析构函数的那层)
  • 相关阅读:
    2020-2021-1 20201221 《信息安全专业导论》第五周学习总结
    XOR加密
    2020-2021-1 20201221 《信息安全专业导论》第四周学习总结
    [SQL]创建数据库
    [SQL]基本表的定义及其完整性约束
    [SQL]修改和删除基本表
    [SQL]连接查询
    [SQL]嵌套查询
    机器学习中常用的求导公式
    [C++]-unordered_map 映射
  • 原文地址:https://www.cnblogs.com/penghuster/p/6260511.html
Copyright © 2020-2023  润新知