为什么从构造函数抛出异常时会发生内存泄漏?

Why is there a memory leak when an exception is thrown from a constructor?

我读了 C++ How to Program 8th Edition 一书 Paul Deitel。在第 645 页有一个声明:

When an exception is thrown from the constructor for an object that's created in a new expression, the dynamically allocated memory for that object is released.

为了验证这个说法,我写了如下代码:

#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的析构函数没有被调用,这似乎与上面的说法冲突。

我的代码是否正确以验证声明?如果不是,我应该如何修改它?如果是,是不是说明这个说法是错误的?

这意味着 B 的 ctor 中直到异常点的所有内容都是销毁的。 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 的构造函数永远不会被替换为您的新 class 的构造函数,采用指针:

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;
}

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

你混淆了两件事:

  • 正在释放内存
  • 被调用的析构函数

你已经证明后者不会发生,这是有道理的:你怎么能摧毁没有正确构造的东西?请注意,成员变量 调用其析构函数,因为在构造函数抛出异常时所有成员变量都已完全构造。

但这与释放内存无关,肯定会

[C++11: 15.2/2]: An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects (excluding the variant members of a union-like class), that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked. If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.5), if any, is called to free the storage occupied by the object.