【问题标题】:Destructor not invoked when an exception is thrown in the constructor构造函数中抛出异常时未调用析构函数
【发布时间】:2012-04-15 20:08:16
【问题描述】:

为什么这段代码没有调用析构函数?

#include <boost/scoped_ptr.hpp>
#include <iostream>

class MyClass {
boost::scoped_ptr<int> ptr;
public:
MyClass() : ptr(new int) { *ptr = 0; throw; std::cout<<"MyClass Allocated\n"; }
~MyClass() { std::cout<<"MyClass De-allocated\n"; }
int increment() { return ++*ptr; }
};

int main()
{
    boost::scoped_ptr<MyClass> myinst(new MyClass);
    std::cout << myinst->increment() << '\n';
    std::cout << myinst->increment() << '\n';
}

编辑

从答案中了解到,当构造函数发生异常时,不会调用析构函数。但是如果异常发生在main()中,即MyClass对象被完全实例化后,会调用MyClass的析构函数吗?如果不是,那为什么是智能指针?

添加代码

#include <boost/scoped_ptr.hpp>
#include <iostream>

class MyClass {
    boost::scoped_ptr<int> ptr;
    public:
    MyClass() : ptr(new int) { *ptr = 0; std::cout<<"MyClass Allocated\n"; }
    ~MyClass() { std::cout<<"MyClass De-allocated\n"; }
    int increment() { return ++*ptr; }
};

int main()
{
    boost::scoped_ptr<MyClass> myinst(new MyClass);
    throw 3;
    std::cout << myinst->increment() << '\n';
    std::cout << myinst->increment() << '\n';
}

输出:

MyClass Allocated
terminate called after throwing an instance of 'int'
Aborted

【问题讨论】:

  • 请注意这里的throw; 会导致未定义的行为。你需要扔东西。 throw; 语句仅在 catch 子句中有效,其中异常已处于活动状态。

标签: c++ boost scoped-ptr


【解决方案1】:

C++ 对象的生命周期仅在其构造函数成功完成后才开始。
由于在构造函数调用完成之前引发了异常,因此您没有完整的对象,因此没有析构函数。

Herb Sutter explains this nicely,引用他的话:

问: 从构造函数发出异常是什么意思?

A:这意味着构造失败,对象从未存在过,它的生命周期从未开始。实际上,报告构造失败(即无法正确构建给定类型的功能对象)的唯一方法是抛出异常。 (是的,有一个现在已经过时的编程约定说,“如果遇到麻烦,只需将状态标志设置为 'bad' 并让调用者通过 IsOK() 函数对其进行检查。”我现在会对此发表评论.)

在生物学方面,
受孕发生了 - 构造函数开始了 - 但尽管尽了最大努力,但还是流产了 - 构造函数从未运行到终止(终止)。 em>

顺便说一句,这就是为什么如果构造函数没有成功就永远不会调用析构函数的原因——没有什么可以销毁的。 "It cannot die, for it never lived." 请注意,这使得短语 "an object whose constructor threw an exception" 真的是矛盾的。这样的东西甚至比以前的对象还要少……它从未存在过,从未存在过,从未呼吸过它的第一次呼吸。它是一个非对象。

我们可以将 C++ 构造函数模型总结如下:

要么:

(a) 构造函数到达其结束或返回语句正常返回,并且对象存在。

或者:

(b) 构造函数抛出异常退出,对象不仅现在不存在,而且从未作为对象存在过。

编辑 1:
但是如果异常发生在 main(),即在 MyClass 对象完全实例化之后,会调用 MyClass 析构函数吗?

是的,会的!
这就是使用scoped_ptr的目的,一旦main中抛出异常,Stack Unwinding将导致所有本地对象被释放,这意味着myinst(驻留在堆栈上)也将被释放,其中turn 将调用MyClass 的析构函数。

如有疑问,请联系 Boost doccumentation

scoped_ptr 类模板存储一个指向动态分配对象的指针。 (动态分配的对象使用 C++ new 表达式进行分配。)保证删除指向的对象,无论是在 scoped_ptr 销毁时,还是通过显式 reset

编辑 2:
为什么您编辑的程序会崩溃?
您的程序显示崩溃是因为,您抛出异常但从未捕获它。当这种情况发生时,会调用一个名为 terminate() 的特殊函数,其默认行为是调用 abort()。在此特定场景中调用 terminate() 之前是否解开堆栈是实现定义的行为Ref 1 sup>.Seems 你的实现没有&你也不应该依赖这种行为。

你可以修改你的程序来处理异常,你应该得到你期望的行为:

#include <boost/scoped_ptr.hpp> 
#include <iostream> 

class MyClass { 
    boost::scoped_ptr<int> ptr; 
    public: 
    MyClass() : ptr(new int) { *ptr = 0; std::cout<<"MyClass Allocated\n"; } 
    ~MyClass() { std::cout<<"MyClass De-allocated\n"; } 
    int increment() { return ++*ptr; } 
}; 

void doSomething()
{
    boost::scoped_ptr<MyClass> myinst(new MyClass); 
    throw 3; 
} 

int main() 
{
    try 
    {
        doSomething();    
    }
    catch(int &obj)
    {
        std::cout<<"Exception Handled";
    }

} 

Ref1C++03 15.5.1 terminate()函数

在以下情况下,必须放弃异常处理以使用不太微妙的错误处理技术:
....
— 当异常处理机制找不到抛出异常的处理程序时(15.3),
....

在这种情况下,

  1. 无效终止();

被称为 (18.6.3)。在没有找到匹配处理程序的情况下,在调用terminate() 之前是否展开堆栈是实现定义的。在所有其他情况下,在调用terminate() 之前,不应展开堆栈。基于展开过程最终将导致对terminate() 的调用的确定,不允许实现过早完成堆栈展开。

【讨论】:

  • 但这不会发生。我在 main() 中放了一个 `throw,程序在没有调用析构函数的情况下中止。
  • @cppcoder:应该。除非你的MyClass析构函数抛出了另一个异常。你能发布代码示例来显示这种行为吗?
【解决方案2】:

因为在这种情况下调用析构函数没有意义。

您只会破坏已构建的事物,而您的对象永远不会完全构建。不过,您的类成员已被构造,并将调用其析构函数。

【讨论】:

  • 如果程序出现异常,scoped_ptr会确保分配的内存被正确销毁。那么在哪种情况下会调用~MyClass
  • @cppcoder:我不确定我是否理解你的问题。因为ptr 在构造函数中完全初始化,所以当您抛出异常时它会被破坏(并释放其内存)。 MyClass 破坏时也会破坏,但这里不是这样。
  • 我的问题是关于这条线的。 boost::scoped_ptr&lt;MyClass&gt; myinst(new MyClass); scoped_ptr 类模板存储了一个指向动态分配对象的指针。那么,不应该发布吗?
  • @cppcoder:它没有什么可释放的,因为它从未持有任何对象。但是,是的,如果它有什么东西,它会释放它所持有的任何东西。
  • 如果异常发生在 main() 而不是构造函数中怎么办?
【解决方案3】:

如果构造函数抛出异常,那么类的析构函数将不会被调用,因为对象还没有完全构造。

查看此链接如何在这种情况下管理资源:

http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.10

【讨论】:

    【解决方案4】:

    当构造函数抛出异常时(调用开始或中途或结束时),则可以确保对象没有被构造。
    因此,不调用从未构造的对象的析构函数是很好的定义。

    这是 Bjarne 网站上的一个相关 FAQ

    【讨论】:

      【解决方案5】:

      从未调用过MyClass 的析构函数,因为从未构造过MyClass 类型的对象。由于抛出异常,每次构造一个的尝试都被中止。

      顺便说一句,如果您希望显示调试消息 - 特别是在处理程序崩溃时 - 您真的应该刷新流:即使用 std::endl '\n' 在行尾。 (或插入std::flush

      虽然仅使用 '\n' 通常有效,但在很多情况下它会失败,并且如果您没有养成正确做事的习惯,那么调试起来真的真的令人困惑。

      【讨论】:

        猜你喜欢
        • 2014-08-02
        • 2012-10-28
        • 2012-04-30
        • 1970-01-01
        • 2013-01-01
        • 2015-08-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多