new [] / delete []并在C ++中抛出构造函数/析构函数

时间:2017-07-17 15:57:05

标签: c++ arrays exception new-operator delete-operator

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

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

我知道内存泄漏被阻止,但另外:

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

  2. 广告(2),尚未被破坏的元素被破坏了吗?如果是,如果析构函数再次抛出会发生什么?

2 个答案:

答案 0 :(得分:5)

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

    • 破坏者不应该扔掉。如果析构函数执行抛出,则会在仍处理上一个(构造函数)异常时发生这种情况。由于不支持嵌套异常,因此会立即调用std::terminate。这是为什么析构函数不应该抛出。
  2. 这里有两个相互排斥的选项:

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

    2. 如果构造函数在步骤(1)中处于中途,则数组x 从未真正存在。该语言尝试为您创建,失败并提出异常 - 因此您根本无法访问(2)

  3. 要理解的关键是x存在 - 处于一个理智且可预测的状态 - 或者它没有。

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

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

答案 1 :(得分:3)

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

#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]

请注意,Destructors在任何时候都没有。这不一定是关键的事情,因为未捕获的异常会使程序崩溃。但如果我们发现错误,我们会看到令人放心的事情:

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没有进行任何资源管理(并且精心设计的程序不会{{1}如果它的构造函数可以像这样抛出资源管理),我们不必担心。但是如果我们的代码看起来像这样,我们可能不得不担心:

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对象会泄漏一个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没有正确的处理代码,即使它已经制作好了#34;安全&#34;通过使用智能指针。但是有些程序员并不合理,即使它们存在,也可能会遇到一种奇怪的,异乎寻常的情况,你必须编写这样的代码。然而,就我而言,教训是始终使用智能指针和其他STL对象来管理动态内存。不要尝试自己动手。在尝试调试时,它会像你一样挽救你的头痛。