在什么情况下C++析构函数不会被调用?

49

我知道我的析构函数会在正常的栈展开和抛出异常时被调用,但在调用exit()函数时不会被调用。

还有其他情况会导致析构函数不被调用吗?比如SIGINT或SIGSEGV等信号。我猜想对于SIGSEGV,析构函数不会被调用,但对于SIGINT会被调用。如何知道哪些信号会展开堆栈?

还有其他情况会导致析构函数不被调用吗?


13
正如这里所指出的:http://thedailywtf.com/Articles/My-Tales.aspx,您还应该意识到,当电源插头被拔掉时,析构函数不会被调用。 ;) - Björn Pollex
4
除非您安装覆盖默认行为的信号处理程序,否则SIGINT不会取消堆栈。 默认情况下,SIGINT会导致程序立即终止。 - karunski
析构函数只在具有静态、自动或线程存储期的对象生命周期结束时(在正常情况下)自动调用。对于具有动态存储期的对象,只有在调用指向该对象的指针的delete时才会调用析构函数。因此,对于从未调用delete的动态对象,析构函数不会被调用(无论是因为内存泄漏使其不可能,还是由于疏忽)。 - Marc van Leeuwen
8个回答

51
有其他情况下析构函数不会被调用吗?
1. 长跳转:它们会干扰自然的堆栈展开过程,并经常导致C++中未定义的行为。 2. 过早退出(您已经指出了这些,但值得注意的是,在由于抛出异常而导致堆栈展开时抛出异常会导致未定义的行为,这就是我们永远不应该从析构函数中抛出异常的原因)。 3. 从构造函数中抛出异常不会调用类的析构函数。这就是为什么如果您在一个构造函数中分配多个由几个不同指针(而不是智能指针)管理的内存块,则需要使用函数级别的try块或避免使用初始化列表并在构造函数体中使用try/catch块(或更好地,只需使用像scoped_ptr这样的智能指针,因为任何成功初始化的成员在初始化列表中都将被销毁,即使类dtor将不会被调用)。 4. 如上所述,当通过基类指针删除类时未将dtor设置为虚拟可能无法调用子类的dtors(未定义的行为)。 5. 对于operator new/new[]调用而未调用匹配的operator delete/delete[](未定义的行为-可能无法调用dtor)。 6. 在使用自定义内存分配器的放置new中,在释放部分中未手动调用dtor。 7. 使用像memcpy这样仅将一个内存块复制到另一个内存块而不调用复制构造函数的函数。在C++中,mem*函数很危险,因为它们可以跨越类的私有数据,覆盖虚表等。结果通常是未定义的行为。 8. 在不完整类型上实例化某些智能指针(如auto_ptr),请参见此讨论

4
清单不错,但第三点存在一个缺陷:你实际上可以在初始化器列表周围放置try块,查找函数级别的try块:struct X { X() try : x_(42) {} catch (...) {} private: int x_; }; 实际上,您可以将其用于任何函数,例如 void foo() try {} catch (...) {} 但是一些重要的编译器(如VS2008,不知道是否已在后续版本中修复)会 choke on it。尽管如此,有关智能指针的建议仍然有效。 - Fabio Fracassi
1
函数 try 在 GOTW #66 中有详细说明(今天刚好向同事指出...巧合...)。 - Matthieu M.
1
感谢@FabioFracassi提供有关函数级try块的提示。我简直无法相信自己在做C++ 15年了,竟然忘记或从未遇到过这个功能。 - Trebor Rude
MSVC使用OMP指令不能保证销毁“threadprivate”对象。 这是否属于您的列表项之一,还是单独的? MSVC的行为是否符合标准? - Kyle Strand
@Arvid 是正确的;在堆栈展开时抛出异常不是未定义行为。请参见此处:http://en.cppreference.com/w/cpp/error/terminate。 - Craig M. Brandenburg
显示剩余8条评论

7
C++标准并未规定如何处理特定信号 - 许多实现可能不支持SIGINT等。如果调用exit()、abort()或terminate(),析构函数将不会被调用。
编辑: 我刚刚快速搜索了C++标准,发现没有任何规定信号如何与对象生命周期交互的内容 - 或许有比我更懂标准的人能找到相关内容吗?
进一步编辑: 在回答另一个问题时,我在标准中找到了这段话:
在退出作用域时(无论如何),对于所有具有自动存储期(3.7.2)(命名对象或临时对象)并在该作用域中声明的已构造对象,按其声明顺序的相反顺序调用析构函数(12.4)。
因此,似乎在接收到信号时必须调用析构函数。

1
收到信号后,程序控制不会退出作用域。因此,引用的标准不适用。 POSIX信号处理程序的默认行为不执行任何堆栈展开或销毁操作。 - karunski
1
如果安装了信号处理程序,则它肯定超出了范围。 - anon
1
@Neil Butterworth 这是 POSIX 标准信号处理程序:http://opengroup.org/onlinepubs/007908775/xsh/signal.h.html - karunski
1
@karunski POSIX并没有定义C++语言。 - anon
1
@karunski 但是问题是关于C ++的。 - anon
显示剩余3条评论

3
一个信号本身不会影响当前线程的执行,也不会影响析构函数的调用,因为它是一个具有自己堆栈的不同执行上下文,您的对象不存在于其中。这就像中断一样:它在执行上下文之外处理,在处理后控制权将返回到程序中。
与多线程一样,C++语言不知道信号的概念。这两者完全无关,并由两个不相关的标准指定。它们如何交互取决于实现,只要不违反任何标准即可。
顺便说一句,当对象的构造函数抛出异常时,析构函数也不会被调用。但成员的析构函数仍然会被调用。

3
如果您正在使用多态并且未将基类析构函数声明为虚函数,则不会调用它们。

1
在这种情况下,您将获得未定义的行为。 - anon

2
abort 函数会终止程序,但不会执行自动或静态存储期对象的析构函数,这是标准规定的。对于其他情况,您应阅读实现特定的文档。

2

如果一个函数或方法有throws规范,并且抛出的内容不在规范范围内,则默认行为是立即退出。堆栈不会被展开,析构函数也不会被调用。

POSIX信号是特定于操作系统的构造,没有C++对象作用域的概念。通常情况下,你不能对信号做任何事情,除了可能捕获它,设置一个全局标志变量,然后在信号处理程序退出后在你的C++代码中处理它。

近期版本的GCC允许您在同步信号处理程序中抛出异常,这确实会导致预期的展开和销毁过程。但这非常依赖操作系统和编译器。


2

这似乎是一个错误,我怀疑C++规范不允许这样的行为。 - WilliamKF
这里的问题是:“在什么情况下析构函数不会被调用”。即使这是Visual Studio中的一个错误,它也是对问题的有效回答,因为错误也是一种情况。人们从Google来到这里,其中一些人可能会遇到这个特定的问题,而不知道它是否是错误。 - Elmue

1

基本上有两种情况会调用析构函数:在函数末尾(或异常处)的堆栈展开时,如果有人(或引用计数器)调用delete。

一个特殊的情况是在静态对象中发现的-它们通过at_exit在程序结束时被销毁,但这仍然是第二种情况。

离开at_exit的信号可能取决于,kill -9将立即终止进程,其他信号将告诉它退出,但具体如何取决于信号回调。


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