如果构造函数抛出异常,析构函数会被调用吗?

52

寻找C#和C++的答案。(在C#中,用'finalizer'来替换'destructor')


我认为在这里要做的事情是建立一个临时项目(或两个:每种语言一个),然后进行检查以找出答案。 - Joel Coehoorn
1
从命令行编译的乐趣在于你不需要设置项目。只需在文本编辑器中加载文件,轻松编译并运行 :) - Jon Skeet
3
@loyc-etc,C++和C#在语义上有很大的不同。这实际上是两个不同的问题。 - Onorio Catenacci
8个回答

58

以下代码可以在C#中实现,但是在C++中不行。

using System;

class Test
{
    Test()
    {
        throw new Exception();
    }

    ~Test()
    {
        Console.WriteLine("Finalized");
    }

    static void Main()
    {
        try
        {
            new Test();
        }
        catch {}
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

这将打印出“Finalized”


这可能适用于C#,但不适用于C++。 - Matt Dillard
我以为在C#中Test是一个Finalizer,而不是Destructor?啊,我明白了,语法是为设置Finalizer而存在的语法糖。如果我的构造函数可能会抛出异常,我希望在构造函数的最后一步手动添加Finalizer,以避免清理不存在的混乱。 - Logan Capaldo
@Logan:这取决于你读的C#规范的版本,他们是称之为“finalizer”还是“destructor”。 - Jon Skeet
@Jon:除了Ecma-334之外,还有其他C#规范吗? 我知道它们存在,事实上它们已经发布在你的网站上了(http://csharpindepth.com/articles/chapter1/Specifications.aspx) 我的意思是...太糟糕了,没有标准规范适用于高于2.0版本。 话虽如此(抗议)...... C#Finalizer的语义与C ++ Destructor不同(显然,您知道它)。 在C#示例中,我认为将Finalizer替换为Disposer更为合适。 - Fernando Pelliccioni
@FernandoPelliccioni:没有比这个更晚的ECMA规范版本。至于处理 - 问题明确地要求使用终结器。将其更改为使用处理不会回答问题。 - Jon Skeet

56

前言:Herb Sutter在该主题上有一篇很棒的文章:

http://herbsutter.wordpress.com/2008/07/25/constructor-exceptions-in-c-c-and-java/

C++:是和否

如果一个对象的构造函数抛出异常(对象“不存在”),则其析构函数不会被调用,但其内部对象的析构函数可能会被调用。

总之,对象的所有内部部分(即成员对象)将按照它们的构造顺序的相反顺序调用其析构函数。除非以某种方式使用RAII,否则在构造函数中构建的每个内容都不会被调用其析构函数。

例如:

struct Class
{
   Class() ;
   ~Class() ;
   
   Thing *    m_pThing ;
   Object     m_aObject ;
   Gizmo *    m_pGizmo ;
   Data       m_aData ;
}

Class::Class()
{
   this->m_pThing = new Thing() ;
   this->m_pGizmo = new Gizmo() ;
}

创建顺序将是:

  1. 调用m_aObject的构造函数。
  2. 调用m_aData的构造函数。
  3. 调用Class构造函数。
  4. 在Class构造函数内部,将调用m_pThing的new操作符和其构造函数。
  5. 在Class构造函数内部,将调用m_pGizmo的new操作符和其构造函数。

假设我们正在使用以下代码:

Class pClass = new Class() ;

一些可能的情况:

  • 如果在构造函数中m_aData抛出异常,那么m_aObject的析构函数将会被调用。然后,由“new Class”分配的内存将被释放。

  • 如果在new Thing(内存不足)时m_pThing抛出异常,则m_aData和m_aObject的析构函数将被调用。然后,由new Class分配的内存将被释放。

  • 如果在构造函数中m_pThing抛出异常,则由“new Thing”分配的内存将被释放。然后,m_aData和m_aObject的析构函数将被调用。然后,由new Class分配的内存将被释放。

  • 如果在构造函数中m_pGizmo抛出异常,则由“new Gizmo”分配的内存将被释放。然后,m_aData和m_aObject的析构函数将被调用。然后,由new Class分配的内存将被释放。请注意m_pThing泄露了

如果要提供基本异常保证,则不能泄漏内存,即使在构造函数中也是如此。因此,您必须以这种方式编写代码(使用STL或甚至Boost):

struct Class
{
   Class() ;
   ~Class() ;
   
   std::auto_ptr<Thing>   m_pThing ;
   Object                 m_aObject ;
   std::auto_ptr<Gizmo>   m_pGizmo ;
   Data                   m_aData ;
}

Class::Class()
   : m_pThing(new Thing())
   , m_pGizmo(new Gizmo())
{
}

甚至可以是:

Class::Class()
{
   this->m_pThing.reset(new Thing()) ;
   this->m_pGizmo.reset(new Gizmo()) ;
}

如果你想/需要在构造函数内部创建这些对象。

这样,无论构造函数抛出什么异常,都不会泄露任何东西。


另一种情况,如果 m_pGizmo 在 new 处抛出异常,m_pThing 仍然会泄漏,对吗?谢谢。 - krebstar
不会,因为m_pThing是一个智能指针。如果m_pThing构造正确,那么即使m_pGizmo抛出异常,它的析构函数也会被调用。 - paercebal
Herb Sutter的文章对于C#和Java是错误的。即使构造函数抛出异常,终结器似乎仍然会被执行。 - David Leonard
@paercebal Dispose 不等同于 finalizer。 - Travis
@Travis:你说得对。由于Sutter的文章是关于处理器而不是终结器,所以当David Leonard写道“Sutter的文章是错误的,即使构造函数抛出异常,终结器似乎仍然会运行”,我错误地认为他搞错了名称(因此,在Sutter的示例中,我的回答是关于C#处理器或Java的finally子句)。好的是,你的评论让我有机会阅读Sutter关于终结器执行的其他评论:这是不使用它们的另一个原因... - paercebal
显示剩余2条评论

12

因为对象从未完全构建,所以正在构建的类的析构函数不会被调用。

然而,只要成为基类对象,就会调用其基类(如果有)的析构函数。

此外,任何成员变量也将调用它们自己的析构函数(正如其他人所指出的那样)。

注意:此适用于C ++。


"所有基类的析构函数",严谨点说。标准中详细阐述了在多重继承时,当一个构造函数抛出异常时它是如何工作的。 - MSalters

2
在C++中,对象的析构函数不会被调用。但是,对象中任何成员数据的析构函数会被调用,除非在构建其中一个成员时抛出了异常。在C++中,成员数据按照声明的顺序初始化(即构造),因此当构造函数抛出异常时,已经初始化的所有成员数据(无论是在成员初始化列表(MIL)中显式初始化还是其他方式)都将以相反的顺序被拆除。

但需要注意的是,使用new初始化的原始指针成员将不会自动删除。 - Michael Burr
他说得很正确。如果你有一个用new T初始化的T*成员,那么你实际上有两个对象:新的T和T*本身。就像int一样,T*没有析构函数。因此,当删除T*成员时,指向的T并不会被删除。 - MSalters
同意。通常情况下,删除指针的任务应该放在析构函数中,但由于析构函数没有被调用,所以会导致内存泄漏(除非在构造函数中编写try/catch来处理这个问题)。更安全的选择是使用自动指针或智能指针。 - Matt Dillard

1
如果构造函数没有执行完毕,对象就不存在,因此也就没有什么可以析构的了。这是关于C++的,我对C#一无所知。

1

对于C ++,这在先前的问题中得到了解决:下面的代码会导致C++内存泄漏吗?

由于在C ++中,当构造函数中抛出异常时,析构函数不会被调用,但已构造的对象成员的dtors确实会被调用,这是使用智能指针对象而不是原始指针的主要原因 - 它们是防止此类情况下的内存泄漏的好方法。


0

C++ -

不是的。对于部分构造的对象,析构函数不会被调用。一个注意点:对于完全构造的成员对象,析构函数会被调用。(包括自动对象和本地类型)

顺便说一句 - 你真正要找的是所谓的“栈展开”。


实际上,我正在编写一个类似STL的集合,所以我手动调用构造函数和析构函数。因此,我想知道在异常情况下调用析构函数是否是正常的。但是你描述的这种“部分销毁”必须由编译器完成,所以我不必担心。 - Qwertie

0

不要在构造函数中执行可能引发异常的操作。

在构造函数之后调用一个可以抛出异常的Initialize()方法。


请问,您能解释一下为什么吗?在大多数情况下,“Initialize()”惯用语在C++中是反模式。也许您误解了“永远不要从析构函数中抛出异常”的概念? - paercebal
我知道,设计一个带有“Initialize()”的类完全违背了拥有类的初衷。 - user15071
创建带有init方法的类有很多合理的原因,特别是在处理并发时。有时候,即使对象的主要初始化很昂贵,你也需要让对象的创建变得便宜。 - Herms
Google的代码指南不鼓励使用异常,因此他们不鼓励从构造函数中抛出异常(http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions)。 - Max Lybbert
另一方面,几乎所有其他人都建议从构造函数中抛出异常,因为这是唯一的信号构造失败的方式(http://herbsutter.wordpress.com/2008/07/25/constructor-exceptions-in-c-c-and-java/)。 - Max Lybbert

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