在下面的代码中,如果构造/销毁某些数组元素会发生什么?
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道内存泄漏被阻止,但另外:
Ad(1),先前构造的元素是否被破坏?如果是,如果析构函数抛出这种情况会发生什么?
广告(2),尚未被破坏的元素被破坏了吗?如果是,如果析构函数再次抛出会发生什么?
答案 0 :(得分:5)
是的,如果x[5]
的构造函数抛出,那么已经成功构造的五个数组元素x[0]..x[4]
将被正确销毁。
std::terminate
。这是为什么析构函数不应该抛出。这里有两个相互排斥的选项:
如果您到达标签(2)
,则构造函数没有投掷。也就是说,如果x
成功创建,则成功构建了所有十个元素。在这种情况下,是的,它们都被删除了。不,你的析构函数仍然不应该抛出。
如果构造函数在步骤(1)
中处于中途,则数组x
从未真正存在。该语言尝试为您创建,失败并提出异常 - 因此您根本无法访问(2)
。
要理解的关键是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对象来管理动态内存。不要尝试自己动手。在尝试调试时,它会像你一样挽救你的头痛。