为什么在构造函数抛出异常时会导致内存泄漏?

3
我读了Paul Deitel所著的《C++ How to Program 第8版》。在第645页有这样一句话:

当在new表达式中创建一个对象时,如果其构造函数抛出异常,则会释放该对象的动态分配内存。

为了验证这个说法,我编写了以下代码:
#include <iostream>
#include <exception>
#include <memory>

class A{
public:
  A(){std::cout << "A is coming." << std::endl;}
  ~A(){std::cout << "A is leaving." << std::endl;}
};
class B
{
public:
  B()
  {
    std::cout << "B is coming." << std::endl;
    A b;
    throw 3;
  }
  ~B(){std::cout << "B is leaving." << std::endl;}
};

int main(void)
{
    try
    {
        std::shared_ptr<B> pi(new B);
    }
    catch(...)
    {
      std::cout << "Exception handled!" << std::endl;
    }
}

输出结果为:
B is coming.
A is coming.
A is leaving.
Exception handled!

这表明B的析构函数未被调用,这似乎与上面的声明相矛盾。
我的代码是否正确验证了该语句?如果不是,我应该如何修改它?如果是,这是否意味着该语句是错误的?

1
仅仅因为析构函数没有被调用,并不意味着它的内存泄漏了。我在你的代码中没有看到任何内存泄漏。 - Fred Larson
2个回答

5
您混淆了两件事情:
1. 释放内存。 2. 调用析构函数。
您已经证明后者不会发生,这是有道理的:您如何销毁没有正确构建的东西?请注意,成员变量将启动其析构函数,因为在构造函数抛出异常时,所有成员变量都已完全构建。
但这与释放内存无关,内存肯定会被释放。
引用块中提到:“对于任何存储期对象,其初始化或析构由异常终止,将执行所有完全构造的子对象的析构函数(不包括类似联合体的变体成员),即已完成执行主构造函数(12.6.2)且析构函数尚未开始执行的子对象。同样,如果该对象的非委托构造函数已完成执行,并且该对象的委托构造函数以异常退出,则将调用该对象的析构函数。如果该对象是在new-expression中分配的,则调用匹配的dealloc函数(3.7.4.2、5.3.4、12.5),如果有的话,以释放所占用的存储器。”

2

这意味着在 B 的构造函数中,在异常抛出的那一点之前的所有内容都会被销毁。因为 B 实例本身从未被构造,所以不应该被销毁。同时注意到 pi 也从未被构造。

std::shared_ptr<B> pi(new B)  - start with new B
new B                         - triggers the ctor of B
std::cout ...                 - the output 
A b;                          - construct an A
throw 3;                      - calls ~A()
                              - rewind, new B is "aborted"
                              - std::shared_ptr<B> pi(new B) is "aborted"

您可以修改代码以查看,通过替换为一个新的类并使用指针,std::shared_ptr 的构造函数将永远不会被触发:

struct T {
  T(B*) { std::cout << "T::T()\n"; }
};
...
try
{
    T pi(new B);  // instead of std::shared_ptr<B> pi(new B);
}
...
T的构造函数不会被调用(参见“pi从未被构造”)。
现在假设B的构造函数将分配内存,如下所示:
B()
{
  A* a = new A();   // in contrast to A a;
  throw 3;
}

之前调用了A::~A(),即a被销毁,现在我们有一个指针,指针不需要被销毁。但是分配给a的内存没有被删除。(如果你使用智能指针std::unique_ptr<A> a = std::make_unique<A>();,内存将被释放,因为调用了std::unique_ptr<A>的析构函数,它会释放内存。)


1
这就是为什么智能指针很重要的原因之一 - 如果 B 的构造函数在异常之前对一个普通指针进行了原始内存分配,那么就不会自动释放 内存。 - Michael Burr
@MichaWiedenmann 对不起,我还是有点不确定你的答案。我们怎么知道新的B被“中止了,换句话说,pi从未被构建”?你能在理论上解释一下或提供证明代码吗? - Neymar87
1
@Neymar87 在调用任何函数时,包括构造函数,在调用函数之前会完全评估组成参数的所有表达式。因此,由于new B是调用构造pi的参数,并且由于new B由于抛出异常而从未完成,因此pi的构造函数从未被执行。 - jared_schmitz
你能建议在创建自由存储器上的 A 时,在 B 的构造函数内使用 unique ptr 而不是 shared ptr 吗?暗示 shared ptr 应该是默认选项是一个坏主意。 - Yakk - Adam Nevraumont
@Yakk 当然,你被鼓励下次自己编辑帖子,这是StackOverflow,我们关心的是最好的答案,而不是写它的人。 - Micha Wiedenmann

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