析构函数详解
文章目录
析构函数
在有两种情况下会调用析构函数。第一种是当对象在正常状态下被销毁,也就是当它离开了它的生存空间或是被明确的删除。第二种是当对象被异常处理机制-栈展开-销毁.
默认析构函数
如果类没有定义析构函数,那么只有在类内含的成员对象(或类的基类)拥有析构函数的情况下,编译器才自动合成出一个来.否则,析构函数被视为不需要,也就不需被合成.
继承体系下的析构函数
析构函数的执行顺序与构造函数的执行顺序相反,如下:
- 析构函数的本题首先被执行
- 如果类有成员类对象,而后者拥有析构函数,那么它们会以其声明顺序的相反顺序被调用.
- 如果类有vptr,现在被重新设定,指向适当的基类的需表.
- 如果有任何直接的(上一层)基类拥有析构函数,它们会以声明顺序的相反顺序被调用
- 如果有任何虚基类拥有析构函数,而目前讨论的这个类是最尾端的类,那么它们会以其原来的构造顺序的相反顺序被调用.
为多态基类声明virtual析构函数
当derived class对象经由一个baseclass指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义.实际执行时通常发生的是对象的derived成分没被销毁。然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。这可是形成资源泄漏、败坏之数据结构、在调试器上浪费许多时间的绝佳途径喔。
消除这个问题的做法很简单:给base class 一个virtual析构函数。此后删除derived class对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class 成分:
class TimeKeeper {
public:
TimeKeeper();
virtual 〜TimeKeeper();
};
TimeKeeper* ptk = getTimeKeeper();
delete ptk; //现在,行为正确。
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。
欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由个所谓vptr (virtual table pointer)指针指出。
virtual函数的实现细节不重要。重要的是如果class内含virtual函数,其对象的体积会增加vptr.对象也不再和其他语言(如C)内的相同声明有着一样的结构(因为其他语言的对应物并没有vptr),因此也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿vptr——那属于实现细节,也因此不再具有移植性。
因此,无端地将所有classes的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。
别让异常逃离析构函数
综上所述,我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的栈展开(stack-unwinding)的过程中,防止 terminate 被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
确保析构函数能完成任务
考虑以下代码:
class Widget {
public:
~Widget{ ){...} //假设这个可能吐出一个异常
};
void doSomething()
{
std::vector<Widget> v;
} //v在这里被自动销毁
当vector v被销毁,它有责任销毁其内含的所有widgets。假设v内含十个Widgets,而在析构第一个元素期间,有个异常被抛出。其他九个Widgets还是应该被销毁(否则它们保存的任何资源都会发生泄漏),因此v应该调用它们各个析构函数。但假设在那些调用期间,第二个Widget析构函数又抛出异常„现在有两个同时作用的异常,这对C++而言太多了。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。
容器或array并非遇上麻烦的必要条件,只要析构函数吐出异常,即使并非使用容器或arrays,程序也可能过早结束或出现不明确行为。
防止 terminate 被调用
调用析构函数时异常可能处于激活状态也可能没有处于激活状态。没有办法在析构函数内部区分出这两种情况。因此在写析构函数时你必须保守地假设有异常被激活。因为如果在一个异常被激活的同时,析构函数也抛出异常,并导致程序控制权转移到析构函数外,C++将调用 terminate 函数。这个函数的作用正如其名字所表示的: 它终止你程序的运行,而且是立即终止,甚至连局部对象都没有被释放。
下面举一个例子,
class Session {
public:
Session();
~Session();
...
private:
static void logCreation(Session *objAddr);
static void logDestruction(Session *objAddr);
};
函数 logCreation 和 logDestruction 被分别用于记录对象的建立与释放。我们因此可以这样编写 Session 的析构函数:
Session::~Session()
{
logDestruction(this);
}
如果 logDestruction 抛出一个异常,会发生什么事呢?异常没有被 Session 的析构函数捕获住,所以它被传递到析构函数的调用者那里。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么 terminate 函数将被自动调用,彻底终止你的程序。
解决方法
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
假设你使用一个class负责数据库连接:
class DBConnection {
public:
static DBConnection create()
void close()
};
创建一个用来管理DBConnection资源的class,并在其析构函数中调用close。
class DBConn { //这个 class 用来管理 DBConnection 对象
public:
~DBConn () //确保数据库连接总是会被关闭
{
db.close();
}
private:
DBConnection db;
};
只要调用close成功,一切都美好。但如果该调用导致异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。那会造成问题,因为那就是抛出了难以驾驭的麻烦。
我们可以在 catch 中放入 try,但是这总得有一个限度,否则会陷入循环。因此我们在 释放 Session 时必须忽略掉所有它抛出的异常:
Session::~DBConn()
{
try {
db.close();
}
catch (...) { }
}
catch 阻止了任何从db.close() 抛出的异常被传递到 DBConn 析构函数的外面。我们现在能高枕无忧了, 无论DBConn对象是不是在exception机制中被释放,terminate 函数都不会被调用。
另一个较佳策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。例如DBConn自己可以提供一个close函数,因而赋予客户一个机会得以处理“因该操作而发生的异常”。DBConn也可以追踪其所管理之DBConnection是否已被关闭,并在答案为否的情况下由其析构函数关闭之。这可防止遗失数据库连接。然而如果DBConnection析构函数调用close失败,我们又将退回“强迫结束程序”或“吞下异常”的老路:
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常就是危险,总会带来“过早结束程序”或“发生不明确行为”的风险。
本例要说的是,由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会,否则他们没机会响应。如果他们不认为这个机会有用(或许他们坚信不会有错误发生),可以忽略它,倚赖DBConn析构函数去调用close。如果真有错误发生——如果close的确抛出异常一-而且DBConn吞下该异常或结束程序,客户没有立场抱怨,毕竟他们曾有机会第一手处理问题,而他们选择了放弃。
文章作者 Forz
上次更新 2017-08-30