C++类的析构函数如果抛出异常会被调用吗?

6
假设我有这样一个类:
#include <iostream>

using namespace std;

class Boda {
    private:
        char *ptr;

    public:
        Boda() {
            ptr = new char [20];
        }
        ~Boda() {
            cout << "calling ~Boda\n";

            delete [] ptr;
        }

        void ouch() {
            throw 99;
        }
};

void bad() {
    Boda b;
    b.ouch();
}

int main() {
    bad();
}

看起来析构函数 ~Boda 从未被调用,因此 ptr 资源永远不会被释放。

以下是程序的输出:

terminate called after throwing an instance of 'int'
Aborted

看起来我的问题的答案是不是

但我认为当异常被抛出时,栈会被展开?为什么在我的示例中Boda b对象没有被销毁?

请帮助我理解这个资源问题。我想将来写出更好的程序。

那么,这就是所谓的RAII吗?

谢谢,Boda Cydo。

3个回答

9
如果异常没有被任何地方捕获,那么C++运行时可以直接终止程序而不进行任何堆栈展开或调用任何析构函数。
但是,如果在调用bad()的地方添加try-catch块,您将看到调用Boda对象的析构函数:
int main() {
    try {
      bad();
    } catch(...) {  // Catch any exception, forcing stack unwinding always
      return -1;
    }
}

RAII意味着动态(堆)分配的内存总是由自动(栈)分配的对象拥有,并在对象销毁时释放它。这依赖于析构函数将在自动分配的对象超出作用域时被调用的保证,无论是由于正常返回还是由于异常。
通常情况下,这种边角情况不会对RAII造成问题,因为通常您希望析构函数运行的主要原因是释放内存,并且当程序终止时,所有内存都会归还给操作系统。但是,如果您的析构函数执行更复杂的操作,比如删除磁盘上的锁定文件之类的操作,那么当程序崩溃时,是否调用析构函数将会有所区别,您可能需要在try-catch块中包装您的main函数以捕获所有异常(只是为了在异常时退出),以确保在终止之前始终解开栈。

是的,如果您有一个将管理某些复杂组合的动态内存的对象,则需要跟踪其实际分配和未分配的内存,以便在其析构函数中删除正确的内容。但是,只要确保没有分配内存的指针始终设置为“NULL”,这就很容易做到。而且通常情况下您也不会自己编写这样的类。通常您希望使用STL容器或智能指针。 - Tyler McHenry
哦,是的。我忘了如果它们是“NULL”,那就没问题了。 - bodacydo
哇,这就是 Stack Overflow 如此伟大的原因,感谢 @Tyler。 - David Gladfelter
但是,如果我有另一个复杂的情况,例如我已经调用了 init_library_xinit_library_y 这些 C 语言库,那该怎么办呢?那么我就必须保持 lib_x_initedlib_y_inited 标志,对吧?在使用 C 代码库时似乎存在这种模式,所以您能否对“你不常编写这样的类”发表一些评论? - bodacydo
David Gladfelter,是的。我非常喜欢这个地方。如果人们不友好和乐于助人,我会成为一个更糟糕的程序员!谢谢Tyler、Nikolai和其他所有人! :) - bodacydo
显示剩余7条评论

2

如果构造函数中发生异常,析构函数将不会被执行。

如果在另一个方法中引发了异常,例如您的示例中,如果必要的话(如果异常在某处得到处理),它将被运行。但是,由于程序被终止,这里不需要调用析构函数,行为取决于编译器...

RAII 的思想是构造函数分配资源并释放它们。如果构造函数中发生异常,则没有简单的方法可以知道哪些资源已分配,哪些未分配(这取决于异常发生的确切位置)。您还应该记住,如果构造函数失败,告诉调用者的唯一方法是引发异常 并且 分配的内存被释放(无论是堆栈展开还是堆分配的内存),就好像从未分配过一样。

解决方案很明显:如果构造函数中可能发生任何异常,则必须捕获它并在必要时释放已分配的资源。实际上,这可能是一些重复的代码与析构函数,但这不是一个大问题。

在析构函数中,不应引发异常,因为它可能导致堆栈展开出现大问题。

在任何其他方法中,根据需要使用异常,但不要忘记在某处处理它们。未处理的异常可能比没有异常更糟糕。我知道一些程序不处理某些次要错误的异常...并且会因应该只发出警告的错误而崩溃。


1
尝试刷新流-您将看到确实调用了析构函数:
cout << "calling ~Boda" << endl;

是I/O的缓冲导致输出被延迟,直到程序终止才会实际输出。

编辑:

上述适用于已处理的异常。对于未处理的异常,标准没有规定是否展开堆栈。另请参见this SO question


嵌入的\n将刷新流,假设cout指向终端。 - Tyler McHenry
@Tyler:你能指出一个地方说明嵌入的\n会刷新终端吗?我发现这并不是事实,但我不确定我是否只在文件中注意到了。 - Stephen
哦,谁告诉你的?请阅读这个链接:http://www.cplusplus.com/doc/tutorial/basic_io/ - Nikolai Fetissov
1
抱歉,经过进一步阅读,我认为flush-on-\n是从C99带过来的gcc主义。 - Tyler McHenry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接