C++ 中的 new[] / delete[] 和抛出构造函数/析构函数
new[] / delete[] and throwing constructors / destructors in C++
在下面的代码中,如果某些数组元素的构造/销毁抛出异常,会发生什么情况?
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道可以防止内存泄漏,但另外:
Ad(1),之前构造的元素是否被破坏了?如果是,如果析构函数在这种情况下抛出会发生什么?
广告(2),未销毁的元素是否销毁?如果是,如果析构函数再次抛出会发生什么?
是的,如果x[5]
的构造函数抛出,那么x[0]..x[4]
已经构造成功的5个数组元素会被正确销毁
- 析构函数不应抛出。如果析构函数 确实 抛出,这会在前一个(构造函数)异常仍在处理时发生。由于不支持嵌套异常,因此会立即调用
std::terminate
。这就是为什么 析构函数不应该抛出。
这里有两个互斥的选项:
如果你到达标签(2)
,构造函数没有抛出。即如果x
创建成功,则十个元素全部构建成功。在这种情况下,是的,它们都会被删除。不,你的析构函数仍然不应该抛出。
如果构造函数在步骤 (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 对象来管理动态内存。不要尝试自己动手。当你尝试调试东西时,它会像这样让你头疼。
在下面的代码中,如果某些数组元素的构造/销毁抛出异常,会发生什么情况?
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道可以防止内存泄漏,但另外:
Ad(1),之前构造的元素是否被破坏了?如果是,如果析构函数在这种情况下抛出会发生什么?
广告(2),未销毁的元素是否销毁?如果是,如果析构函数再次抛出会发生什么?
是的,如果
x[5]
的构造函数抛出,那么x[0]..x[4]
已经构造成功的5个数组元素会被正确销毁- 析构函数不应抛出。如果析构函数 确实 抛出,这会在前一个(构造函数)异常仍在处理时发生。由于不支持嵌套异常,因此会立即调用
std::terminate
。这就是为什么 析构函数不应该抛出。
- 析构函数不应抛出。如果析构函数 确实 抛出,这会在前一个(构造函数)异常仍在处理时发生。由于不支持嵌套异常,因此会立即调用
这里有两个互斥的选项:
如果你到达标签
(2)
,构造函数没有抛出。即如果x
创建成功,则十个元素全部构建成功。在这种情况下,是的,它们都会被删除。不,你的析构函数仍然不应该抛出。如果构造函数在步骤
(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 对象来管理动态内存。不要尝试自己动手。当你尝试调试东西时,它会像这样让你头疼。