C++中的析构函数不要抛出异常

简介: C++中的析构函数不要抛出异常

1.栈展开


为了更好的理解C++中的异常处理机制,先解释一下什么是栈展开。如下例所示:


1#include <iostream>
 2
 3using namespace std;
 4
 5void f1() throw(int)
 6{ // 函数f1会抛出一个整型的异常代码
 7    cout << "f1 starts" << endl;
 8    int i;                     // 这个变量会在栈展开的过程中被释放资源
 9    throw 100;                 // 抛出异常,程序开始在栈中搜索对应的异常处理器,即开始栈展开
10    cout << "f1 ends" << endl; // 这行代码不会被执行
11}
12
13void f2 throw(int)
14{ // 函数f2调用了f1,所以抛出异常的类型也是整型
15    cout << "f2 starts" << endl;
16    int j;                     // 这个变量也会在栈展开的过程中被释放资源
17    f1();                      // f1没有搜索到对应的异常处理,因此返回到f2搜索
18    cout << "f2 ends" << endl; // 这行代码也不会被执行
19}
20
21void f3()
22{
23    cout << "f3 starts" << endl;
24    try
25    { // 函数f3在try里调用f2,并可能会catch一个整型的异常
26        f2();
27    }
28    catch (int i)
29    { // f2也没有找到异常处理,最后返回了f3并找到了异常处理
30        cout << "exception " << i << endl;
31    }
32    cout << "f3 ends" << endl;
33}
34
35int main()
36{
37    f3();
38    return 0;
39}


在C++中,当有异常抛出时,调用栈中的用来储存函数调用信息的部分,会被按顺序依次搜索,直到找到对应类型异常的处理程序为止。上例中,搜索顺序就是f1->f2->f3。f1没有对应类型的catch块,因此跳到了f2,但f2也没有对应类型的catch块,因此跳到f3才能处理掉这个异常。


上例中,寻找相应异常类型处理的过程就叫做栈展开。同时在这一过程中,当从f1返回到f2时,f1里局部变量i的资源会被清空,即调用了对象i的析构函数。同样,在从f2返回到f3时,f2里局部变量j也会被清空,即调用了对象j的析构函数。


2.建议阻止C++中的析构函数抛出异常


C++并不会阻止在类的析构函数中抛出异常,但这是一个非常不好的做法!由于栈展开的前提是已经存在一个未处理的异常,并且栈展开会自动调用函数本地对象的析构函数。如果此时对象的析构函数又抛出一个异常,就会同时有两个异常出现,但C++最多只能同时处理一个异常。因此,此时的程序会自动调用std::terminate()函数,导致程序的闪退或者崩溃。如下例所示:


1#include <iostream>
 2#include <vector>
 3
 4class Widget{
 5public:
 6    // ...
 7    ~Widget(){
 8        // ...   假设析构函数内部抛出一个异常
 9    }
10};
11
12void doSomething(){
13    std::vector<Widget> v;
14
15    // ...   局部对象v在此处被自动销毁 
16}


当对象v被销毁时,它有责任销毁其内部含有的所有的Widget。假设v内部含有十个Widget,但是在第一个元素析构的时候,有一个异常抛出。此时,其他的九个Widget还是应该被继续销毁。因此,v应该调用它们各自的析构函数。假设在那些析构函数被调用的时候,第二个Widget析构函数也抛出一个异常。此时,C++无法同时处理两个异常,异常程序会结束运行或导致不明行为。注意:使用STL中的其他容器也会出现相同情况。


假设你的析构函数必须执行一个动作,而这个动作可能会在失败时抛出异常,该怎么办?如下例所示:


1#include <iostream>
 2#include <vector>
 3
 4class DBConnection{
 5public:
 6    // ...
 7    static DBConnection create();  // 这个函数会返回DBConnection对象
 8
 9    void close();  // 断开与数据的连接
10};


为了能确保客户不忘记在DBConnection对象身上调用close()函数,一个合适的方法是创建一个管理DBConnection对象的类DBConn,并且在其析构函数中调用close()函数。如下例所示


1#include <iostream>
 2#include <vector>
 3
 4class DBConnection{
 5public:
 6    // ...
 7    static DBConnection create();  // 这个函数会返回DBConnection对象
 8
 9    void close();  // 断开与数据的连接
10};
11
12class DBConn{
13public:
14    // ...
15    ~DBConn(){
16        db.close();  // 为了确保客户不会忘记在DBConnection对象上调用close()函数
17    }
18private:
19    DBConnection db;
20};
21
22// 因此,客户可以写出如下代码:
23
24{  // 开启一个代码块
25    DBConn dbc(DBConnection::create());  // 建立DBConnection对象并由DBConn对象管理
26    // ... 通过DBConn接口使用DBConnection对象
27}  // 在代码块结束时,DBConn对象dbc被销毁,因此自动为DBConnection对象调用close()函数


在上述客户写出的代码块中,如果close()函数调用成功,则一切完美。但是,如果该调用会导致异常,DBConn析构函数就会传播异常,即允许它离开这个析构函数。


3.解决方法


针对上述客户写出的代码块中可能存在的问题,有两种方法可以对DBConn的析构函数进行优化。a.如果close()函数抛出异常就立即主动关闭程序,通常通过调用abort()完成。通过std::abort()函数来主动关闭程序,而不是任由程序在某个随机时刻突然崩溃,这样能减少潜在用户风险。对于某些比较严重的异常,可以使用这个方法。


1DBConn::~DBConn
 2{
 3    try
 4    {
 5        (db.close();
 6    }
 7    catch (...)
 8    {
 9        // 记录访问历史
10        std::abort();
11    }
12}


b.消化因调用close()而产生的异常。栈展开的过程终止于异常被对应类型的catch块捕捉到。因此,在这种情况下只要catch包括了所有可能的异常,析构函数就能消化掉这个异常,防止异常从析构函数中跑出来,从而和别的异常产生冲突。


但是,这种做法的缺点是可能会给程序的稳定运行带来隐患,因为当某些比较严重或者不能处理的异常发生时,我们继续让程序运行,就可能会导致程序的未知行为。如果我们能保证所有的异常都能被正确处理,程序能继续稳定运行,就可以使用这个方法。


1DBConn::~DBConn
 2{
 3    try
 4    {
 5        (db.close();
 6    }
 7    catch (...)
 8    {
 9       // 记录访问历史
10    }
11}


4.更好的解决方法:把可能抛出异常的代码移出析构函数


设计DBConn类的更安全的接口,让其他函数来承担这个风险,而且这样也可以事先在析构函数这样的紧要关头前对异常做出处理。


1#include <iostream>
 2#include <vector>
 3
 4class DBConnection
 5{
 6public:
 7    // ...
 8    static DBConnection create(); // 这个函数会返回DBConnection对象
 9
10    void close(); // 断开与数据的连接
11};
12
13class DBConn
14{
15public:
16    // ...
17    void close(); // 当需要关闭连接,手动调用此函数
18
19    ~DBConn(); // 析构函数虽然还是要留有备用,但不用每次都承担风险了
20
21
22private:
23    DBConnection db;
24    bool closed; // 显示连接是否被手动关闭
25};
26
27void DBConn::close() // 当需要关闭连接,手动调用此函数
28{
29    db.close();
30    closed = true;
31}
32
33DBConn::~DBConn() // 析构函数虽然还是要留有备用,但不用每次都承担风险了
34{
35    if (!closed)
36    {
37        try
38        {
39            db.close();
40        }
41        catch (...)
42        {
43            // 记录访问历史
44            // 消化异常或者主动关闭程序
45        }
46    }
47}


通过上例中的做法:当关闭连接时,我们先手动调用close()方法,这样就算抛出了异常,我们也可以事先处理,然后再调用析构函数。当然,析构函数还是要检查是否被手动关闭并留有备用方案。如果没有被手动关闭,析构函数还是需要在消化掉异常和主动关闭程序中做出选择。


5.总结


(1) 构造函数中绝对不要抛出异常。如果一个被析构函数调用的函数可能会抛出异常,析构函数应该捕捉任何类型的异常,然后消化这样异常或主动关闭程序。

(2) 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类中应该提供一个普通函数(而不是在析构函数中)执行该操作。

相关文章
|
3月前
|
安全 程序员 编译器
【C++】异常
C++异常处理机制允许在程序运行时出现错误时,通过`try`、`catch`和`throw`关键字将错误信息传递回调用栈,进行异常处理。它支持异常的重新抛出、自定义异常体系以及标准库提供的异常类层次结构,如`std::exception`及其派生类。异常处理提高了代码的健壮性和可维护性,但也带来了性能开销和代码复杂性等问题。合理使用异常机制,可以有效提升程序的稳定性和安全性。
74 3
|
3月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
170 4
|
6月前
|
安全 Java 程序员
|
7月前
|
C++
C++ 异常机制问题之捕获异常的问题如何解决
C++ 异常机制问题之捕获异常的问题如何解决
|
7月前
|
C++ 运维
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
开发与运维函数问题之析构函数在C++类中起什么作用如何解决
53 11
|
7月前
|
安全 Java 程序员
【C++11】异常知多少
【C++11】异常知多少
64 7
|
8月前
|
安全 C++
详细解读c++异常模板复习
详细解读c++异常模板复习
37 0
|
8月前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。
|
8月前
|
存储 Java 程序员
【C++航海王:追寻罗杰的编程之路】异常——错误处理方式之一
【C++航海王:追寻罗杰的编程之路】异常——错误处理方式之一
53 0
|
8月前
|
编译器 C语言 C++
【C++】:构造函数和析构函数
【C++】:构造函数和析构函数
60 0