条款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(相对于当前执行构造函数和析构函数的那层)