new []/delete []并在C++中抛出构造函数/析构函数
在下面的代码中,如果某个数组元素的构造/销毁会抛出什么?new []/delete []并在C++中抛出构造函数/析构函数
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道内存泄漏被防止,但额外的:
广告(1)中,先前构造的元件破坏?如果是的话,如果析构函数在这种情况下抛出会发生什么?
广告(2),是否还未被破坏的元素被破坏?如果是的话,如果析构函数再次抛出会发生什么?
-
是,如果
x[5]
构造抛出,然后x[0]..x[4]
已经成功地构建了五个数组元素将被正确地破坏。- 破坏者不应该扔。如果一个析构函数确实抛出,那么在前一个(构造函数)异常仍在处理的时候会发生这种情况。由于不支持嵌套异常,因此立即调用
std::terminate
。这是为什么析构函数不应该抛出。
- 破坏者不应该扔。如果一个析构函数确实抛出,那么在前一个(构造函数)异常仍在处理的时候会发生这种情况。由于不支持嵌套异常,因此立即调用
-
这里有两个相互排斥的选项:
如果达到标签
(2)
,构造没有抛出。也就是说,如果成功创建了x
,则所有十个元素都已成功构建。在这种情况下,是的,它们全部被删除。不,你的析构函数仍然不应该抛出。如果构造函数抛出部分的方式,通过一步
(1)
,则数组x
根本不存在。该语言试图为你创建,失败,并抛出一个例外 - 所以你根本没有达到(2)
。
最关键的事情要明白的是,x
要么存在 - 在一个健全的和可预见的状态 - 要么没有。
如果构造函数失败,语言不会给你一些不可用的半初始化的东西,因为无论如何你都无法做任何事情。 (你甚至不能安全地删除它,因为没有办法跟踪哪些元素被构建,哪些只是随机垃圾)。
这可能有助于将数组视为具有10个数据成员的对象。如果你正在构造这样一个类的实例,并且抛出了基类或成员构造函数之一,那么所有先前构造的基类和成员将以完全相同的方式销毁,并且对象永远不会存在。
我们可以用下面的代码进行测试:
#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
对象的大块不会泄漏内存,但是第六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
对象,所以第六个对象没有其调用的删除器的事实不再是问题。
现在,很多都涉及到草拟代码:即使通过使用智能指针使其变得“安全”,没有合理的程序员也不会有没有适当处理代码的资源管理器throw
。但是有些程序员只是不合理,即使他们是,也可能会遇到一个奇怪的,异乎寻常的场景,你必须编写这样的代码。就我而言,教训是始终使用智能指针和其他STL对象来管理动态内存。不要试图推出自己的。在尝试调试时,它会像这样为您节省头痛。