我们可以用下面的代码进行测试:
#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 对象,因此第六个Wrapper 对象没有调用其删除器这一事实不再是问题。
现在,其中很多都涉及草编代码:即使通过使用智能指针使其“安全”,任何理性的程序员都不会拥有资源管理器throw 而没有正确处理代码。但是有些程序员就是不通情达理的,即使他们是通情达理的,你也可能会遇到一个奇怪的、奇异的场景,你必须编写这样的代码。因此,就我而言,教训是始终使用智能指针和其他 STL 对象来管理动态内存。不要试图自己动手。在尝试调试时,它会为您省去头疼的问题。