C++ 中的 new[] / delete[] 和抛出构造函数/析构函数

new[] / delete[] and throwing constructors / destructors in C++

在下面的代码中,如果某些数组元素的构造/销毁抛出异常,会发生什么情况?

X* x = new X[10]; // (1)
delete[] x;       // (2)

我知道可以防止内存泄漏,但另外:

  1. Ad(1),之前构造的元素是否被破坏了?如果是,如果析构函数在这种情况下抛出会发生什么?

  2. 广告(2),未销毁的元素是否销毁?如果是,如果析构函数再次抛出会发生什么?

  1. 是的,如果x[5]的构造函数抛出,那么x[0]..x[4]已经构造成功的5个数组元素会被正确销毁

    • 析构函数不应抛出。如果析构函数 确实 抛出,这会在前一个(构造函数)异常仍在处理时发生。由于不支持嵌套异常,因此会立即调用 std::terminate。这就是为什么 析构函数不应该抛出。
  2. 这里有两个互斥的选项:

    1. 如果你到达标签(2),构造函数没有抛出。即如果x创建成功,则十个元素全部构建成功。在这种情况下,是的,它们都会被删除。不,你的析构函数仍然不应该抛出。

    2. 如果构造函数在步骤 (1) 中途抛出,则数组 x 从未真正存在过 。该语言试图为您创建它,但失败了,并引发了异常 - 因此您根本没有达到 (2)

要理解的关键是 x 要么存在 - 处于正常且可预测的状态 - 要么不存在。

如果构造函数失败,该语言不会给你一些无法使用的半初始化的东西,因为无论如何你都不能用它做任何事情。 (你甚至不能安全地删除它,因为没有办法跟踪哪些元素是构造的,哪些只是随机垃圾)。

将数组视为具有十个数据成员的对象可能会有所帮助。如果您正在构建这样一个 class 的实例,并且 base-class 或成员构造函数之一抛出异常,则所有先前构造的基类和成员都将以完全相同的方式销毁,并且您的对象永远不会开始存在。

我们可以用下面的代码进行测试:

#include <iostream>

//`Basic` was borrowed from some general-purpose code I use for testing various issues 
//relating to object construction/assignment
struct Basic {
    Basic() { 
        std::cout << "Default-Constructor" << std::endl; 
        static int val = 0;
        if(val++ == 5) throw std::runtime_error("Oops!");
    }
    Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
    Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
    Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
    Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
    ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};

int main() {
    Basic * ptrs = new Basic[10];
    delete[] ptrs;
    return 0;
}

此代码在崩溃前产生以下输出:

Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
[std::runtime_error thrown and uncaught here]

请注意,在任何时候都不会调用析构函数。这不一定是关键的事情,因为未捕获的异常无论如何都会使程序崩溃。但是如果我们发现错误,我们会看到一些令人放心的东西:

int main() {
    try {
        Basic * ptrs = new Basic[10];
        delete[] ptrs;
    } catch (std::runtime_error const& e) {std::cerr << e.what() << std::endl;}
    return 0;
}

输出更改为:

Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Destructor
Destructor
Destructor
Destructor
Destructor
Oops!

因此即使没有显式 delete[] 调用,也会为完全构造的对象自动调用析构函数,因为 new[] 调用具有处理此问题的处理机制。

但是您确实需要担心第六个对象:在我们的例子中,因为 Basic 不进行任何资源管理(并且设计良好的程序不会 Basic资源管理,如果它的构造函数可以像这样抛出),我们不必担心。但是如果我们的代码看起来像这样,我们可能不得不担心:

#include <iostream>

struct Basic {
    Basic() { std::cout << "Default-Constructor" << std::endl; }
    Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
    Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
    Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
    Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
    ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};

class Wrapper {
    Basic * ptr;
public:
    Wrapper() : ptr(new Basic) { 
        std::cout << "WRDefault-Constructor" << std::endl;
        static int val = 0;
        if(val++ == 5) throw std::runtime_error("Oops!");
    }
    Wrapper(Wrapper const&) = delete; //Disabling Copy/Move for simplicity
    ~Wrapper() noexcept { delete ptr; std::cout << "WRDestructor" << std::endl; }
};

int main() {
    try {
        Wrapper * ptrs = new Wrapper[10];
        delete[] ptrs;
    } catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
    return 0;
}

在这里,我们得到这个输出:

Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Oops!

大块Wrapper个对象不会泄漏内存,但是第6个Wrapper个对象会泄漏一个Basic个对象,因为它没有被正确清理!


幸运的是,与任何资源管理方案的情况一样,如果使用智能指针,所有这些问题都会消失:

#include <iostream>
#include<memory>

struct Basic {
    Basic() { std::cout << "Default-Constructor" << std::endl; }
    Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
    Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
    Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
    Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
    ~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};

class Wrapper {
    std::unique_ptr<Basic> ptr;
public:
    Wrapper() : ptr(new Basic) { 
        std::cout << "WRDefault-Constructor" << std::endl;
        static int val = 0;
        if(val++ == 5) throw std::runtime_error("Oops!");
    }
    //Wrapper(Wrapper const&) = delete; //Copy disabled by default, move enabled by default
    ~Wrapper() noexcept { std::cout << "WRDestructor" << std::endl; }
};

int main() {
    try {
        std::unique_ptr<Wrapper[]> ptrs{new Wrapper[10]}; //Or std::make_unique
    } catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
    return 0;
}

并且输出:

Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
Oops!

请注意,对 Destructor 的调用次数现在与对 Default-Constructor 的调用次数一致,这告诉我们 Basic 对象现在已得到正确清理。因为 Wrapper 正在执行的资源管理已委托给 unique_ptr 对象,所以第六个 Wrapper 对象没有调用其删除器这一事实不再是问题。

现在,其中很多涉及草率代码:任何合理的程序员都不会拥有没有适当处理代码的资源管理器 throw,即使它是 "safe" 通过使用智能指针制作的.但是有些程序员就是不讲道理,即使他们讲道理,您也可能会遇到一个奇怪的、奇特的场景,您必须编写这样的代码。那么,就我而言,教训是始终使用智能指针和其他 STL 对象来管理动态内存。不要尝试自己动手。当你尝试调试东西时,它会像这样让你头疼。