简体   繁体   English

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

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

What happens, in the following code, if construction / destruction of some array element throws? 在下面的代码中,如果构造/销毁某些数组元素会发生什么情况?

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

I know that memory leaks are prevented, but additionally: 我知道可以防止内存泄漏,但是另外:

  1. Ad (1), are the previously constructed elements destructed? 广告(1),先前构造的元素是否被破坏? If yes, what happens if destructor throws in such a case? 如果是,那么在这种情况下析构函数抛出该怎么办?

  2. Ad (2), are the not-yet-destructed elements destructed? 广告(2),尚未破坏的元素是否被破坏? If yes, what happens if destructor throws again? 如果是,如果析构函数再次抛出该怎么办?

  1. Yes, if the constructor of x[5] throws, then the five array elements x[0]..x[4] already successfully constructed will be destroyed correctly. 是的,如果x[5]的构造函数抛出异常,则已经成功构造的五个数组元素x[0]..x[4]将被正确销毁。

    • Destructors should not throw. 析构函数不应抛出。 If a destructor does throw, this happens while the previous (constructor) exception is still being handled. 如果析构函数确实抛出,则在仍在处理前一个(构造函数)异常的情况下发生。 As nested exceptions aren't supported, std::terminate is called immediately. 由于不支持嵌套异常,因此会立即调用std::terminate This is why destructors shouldn't throw. 这就是为什么析构函数不应该抛出的原因。
  2. There are two mutually-exclusive options here: 这里有两个互斥的选项:

    1. If you reach label (2) , the constructor didn't throw. 如果到达标签(2) ,则构造函数抛出。 That is, if x was successfully created, all ten elements were successfully constructed. 也就是说,如果成功创建了x ,则所有十个元素都被成功构造。 In this case, yes, they all get deleted. 在这种情况下,是的,它们都将被删除。 No, your destructor still shouldn't throw. 不,您的析构函数仍然不应该抛出。

    2. If the constructor threw part-way through step (1) , then the array x never really existed . 如果构造函数途经步骤(1) ,则数组x 不会真正存在 The language tried to create it for you, failed, and threw an exception - so you don't reach (2) at all. 该语言尝试为您创建它,但失败了,并引发了异常-因此您根本无法达到(2)

The key thing to understand is that x either exists - in a sane and predictable state - or it doesn't. 要理解的关键是x要么存在-处于健全且可预测的状态-要么不存在。

The language doesn't give you some un-usable half-initialized thing, if a constructor failed, because you couldn't do anything with it anyway. 如果构造函数失败,该语言不会给您一些无法使用的半初始化内容,因为您无论如何都无法对其进行任何处理。 (You couldn't even safely delete it, because there would be no way to track which of the elements were constructed, and which were just random garbage). (您甚至无法安全地删除它,因为无法跟踪构造了哪些元素,而哪些只是随机垃圾)。

It might help to consider the array as an object with ten data members. 将数组视为具有十个数据成员的对象可能会有所帮助。 If you're constructing an instance of such a class, and one of the base-class or member constructors throws, all the previously-constructed bases and members are destroyed in exactly the same way and your object never starts existing. 如果要构造此类的实例,并且基类或成员构造函数之一抛出,则所有先前构造的基和成员都将以完全相同的方式销毁,并且对象永远不会开始存在。

We can test with the following code: 我们可以使用以下代码进行测试:

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

This code yields the following output before crashing: 该代码在崩溃前产生以下输出:

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

Note that at no point were the Destructors called. 请注意,绝没有调用析构函数。 This isn't necessarily a critical thing, since an uncaught exception will crash the program anyways. 这不一定是关键的事情,因为未捕获的异常无论如何都会使程序崩溃。 But if we catch the error, we see something reassuring: 但是,如果我们发现了错误,我们就会放心:

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

The output changes to this: 输出更改为:

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

So Destructors will be automatically called for fully constructed objects, even without an explicit delete[] call, because the new[] call has handling mechanisms to deal with this. 因此,即使没有显式delete[]调用,析构函数也将自动调用完全构造的对象,因为new[]调用具有处理此问题的处理机制。

But you do have to worry about that sixth object: in our case, because Basic doesn't do any resource management (and a well-designed program wouldn't have Basic do resource management if its constructor could throw like this), we don't have to worry. 但是您确实要担心第六个对象:在我们的例子中,由于Basic不执行任何资源管理(而且设计良好的程序如果其构造函数可能会抛出此类错误,则Basic不会进行资源管理),因此我们不不必担心。 But we might have to worry if our code looks like this instead: 但是我们可能不得不担心,如果我们的代码看起来像这样:

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

Here, we get this output: 在这里,我们得到以下输出:

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!

The large block of Wrapper objects will not leak memory, but the sixth Wrapper object will leak a Basic object because it was not properly cleaned up! 大块Wrapper对象不会泄漏内存,但是第六个Wrapper对象将泄漏Basic对象,因为未正确清理!


Fortunately, as is usually the case with any resource-management scheme, all these problems go away if you use smart pointers: 幸运的是,与任何资源管理方案一样,如果使用智能指针,所有这些问题都将消失:

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

And the output: 并输出:

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!

Note that the number of calls to Destructor now match the number of calls to Default-Constructor , which tells us that the Basic objects are now getting properly cleaned up. 请注意,现在对Destructor的调用次数与对Default-Constructor的调用次数匹配,这告诉我们Basic对象已得到正确的清理。 And because the resource management that Wrapper was doing has been delegated to the unique_ptr object, the fact that the sixth Wrapper object doesn't have its deleter called is no longer a problem. 并且由于Wrapper正在执行的资源管理已委派给unique_ptr对象,因此第六个Wrapper对象没有调用其deleter的事实不再是问题。

Now, a lot of this involves strawmanned code: no reasonable programmer would ever have a resource manager throw without proper handling code, even if it were made "safe" by use of smart-pointers. 现在,其中很多涉及稻草人的代码:没有合理的程序员,即使没有使用适当的处理代码,也不会throw资源管理器,即使通过使用智能指针使其“安全”。 But some programmers just aren't reasonable, and even if they are, it's possible you might come across a weird, exotic scenario where you have to write code like this. 但是有些程序员只是不合理,即使他们是合理的,您也有可能遇到一个奇怪的,异国情调的场景,您必须编写这样的代码。 The lesson, then, as far as I'm concerned, is to always use smart pointers and other STL objects to manage dynamic memory. 因此,就我而言,该课程是始终使用智能指针和其他STL对象来管理动态内存。 Don't try to roll your own. 不要尝试自己动手。 It'll save you headaches exactly like this when trying to debug things. 尝试调试时,它完全可以像这样省去您的头痛。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM