目录
1.前言
C++并不禁止析构函数吐出异常,但是不鼓励出现这种情况。例如以下代码:
class Widget{
public:
...
~Widget()
{
....//假设这个可能出现异常
}
};
void doSomething()
{
std::vector<Widget> v;
.....//v这里被自动销毁
}
在上述代码中,当vector v被销毁时,它有责任销毁其内含的所有Widgets。假设v内含十个Widgets,而在析构第一个元素期间,有个异常被抛出。其他9个Widgets还是应该被销毁(否则它们保存的任何资源都可能发生资源泄露),因此v会调用它们各个析构函数。但假设在那些调用期间,第二个Widget析构函数又抛出异常。现在有两个同时作用的异常,这对C++而言太多了,因为在两个异常同时存在的情况下,程序不是结束执行,就是出现不明确行为。
2.分析实例
2.1析构函数抛出异常的实例
假设析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?假设存在这样一个class负责数据库连接:
class DBConnection{
public:
....
static DBConnection create();//该函数返回DBConnection对象
.....
void close();关闭联机;失败时会抛出异常
}
为确保不忘记在DBConnection对象身上调用close(),一种方法是创建一个用来管理DBConnection资源的class,并在其析构中调用close。即:
class DBConn{
public:
...
~DBConn()
{
db.close();//确保数据库连接总是会被关闭
}
private:
DBConnection db;
}
于是乎,存在如下逻辑的代码:
class DB{
DBConn dbc(DBConnection::create());//开启一个区块block,建立DBConnection对象并交给DBConn对象以便管理,通过DBConn的接口,使用DBConnection对象。在区块结束点,DBConn对象被销毁,因而自动被DBConnection对象调用close
}
当close调用成功,everything is fine。但是当调用异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。这种情况会导致出现比较麻烦的问题。
2.2解决方法
这里存在3种方法来避免这一问题,DBConn的析构函数:
1.如果close抛出异常就结束程序。通常调用abort来完成;
DBConn::~DBConn()
{
try{db.close();}
catch(...)
{
//制作运行记录,记下对close的失败调用;
std::abort();
}
}
之所以它作为一个解决问题的方法,主要为它可以阻止异常从析构函数传播出去,即调用abort阻止出现不明确错误的行为;
2.吞下因调用close而发生的异常
DBConn::~DBConn()
{
try{db.close();}
catch(...)
{
//制作运转记录,记下对close的失败调用
}
}
3.重新设计DBConn接口,使其有机会对可能出现的问题作出反应。如以下代码逻辑:
class DBConn{
public:
...
void close()
{
db.close();
closed=true;
}
~DBConn()
{
if(!closed)
{
try{//关闭连接
db.close();
}
catch(...)//如果关闭动作失败。记录下来并结束程序或吞下异常
{
//制作运转记录,记下对close的失败调用。
}
}
}
private:
DBConnection db;
bool closed;
};
在上述代码中,把调用close的责任从DBConn析构函数手上转移到DBConn手上。这是因为如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数突出异常就是危险,总会带来过早的结束程序或者发生不明确行为的风险。比如上述代码中,有其本身自己调用close并不会对他们带来负担,而是给它们一个处理错误的机会,否则它们没机会相应。如果它们认为这个机会没用,可以忽略它,依靠DBConn析构函数去调用close。即使有错误发生-即close抛出异常-并且DBConn吞下该异常或结束程序,也不会抱怨,毕竟它们本来有机会第一手处理问题,却选择放弃。