在下文中,我将区分作用域对象和动态对象动态对象,其确切的销毁时间通常要到运行时才能知道。
虽然类对象的析构语义由析构函数决定,但标量对象的析构始终是空操作。具体来说,破坏指针变量不会破坏指针对象。
作用域对象
自动对象
当控制流离开其定义范围时,自动对象(通常称为“局部变量”)将按照其定义的相反顺序被破坏:
void some_function()
{
Foo a;
Foo b;
if (some_condition)
{
Foo y;
Foo z;
} <--- z and y are destructed here
} <--- b and a are destructed here
如果在函数执行过程中抛出异常,所有之前构造的自动对象都会在异常传播到调用者之前被销毁。这个过程称为堆栈展开。在堆栈展开期间,不会有进一步的异常离开上述先前构造的自动对象的析构函数。否则调用函数std::terminate。
这导致了 C++ 中最重要的准则之一:
析构函数永远不应该抛出。
非本地静态对象
在命名空间范围内定义的静态对象(通常称为“全局变量”)和静态数据成员在 main 执行后按其定义的相反顺序被销毁:
struct X
{
static Foo x; // this is only a *declaration*, not a *definition*
};
Foo a;
Foo b;
int main()
{
} <--- y, x, b and a are destructed here
Foo X::x; // this is the respective definition
Foo y;
请注意,在不同翻译单元中定义的静态对象的相对构造(和销毁)顺序是未定义的。
如果异常离开静态对象的析构函数,则调用函数std::terminate。
本地静态对象
在函数内部定义的静态对象在(如果)控制流第一次通过它们的定义时被构造。1
在main执行后,它们以相反的顺序被销毁:
Foo& get_some_Foo()
{
static Foo x;
return x;
}
Bar& get_some_Bar()
{
static Bar y;
return y;
}
int main()
{
get_some_Bar().do_something(); // note that get_some_Bar is called *first*
get_some_Foo().do_something();
} <--- x and y are destructed here // hence y is destructed *last*
如果异常离开静态对象的析构函数,则调用函数std::terminate。
1:这是一个极其简化的模型。静态对象的初始化细节其实要复杂得多。
基类子对象和成员子对象
当控制流离开对象的析构函数体时,其成员子对象(也称为“数据成员”)按其定义的相反顺序被销毁。之后,它的基类子对象会按照 base-specifier-list 的相反顺序被销毁:
class Foo : Bar, Baz
{
Quux x;
Quux y;
public:
~Foo()
{
} <--- y and x are destructed here,
}; followed by the Baz and Bar base class subobjects
如果在Foo 的其中一个子对象的构造期间引发异常,则其先前构造的所有子对象都将在异常传播之前被破坏。另一方面,Foo 析构函数将不会被执行,因为 Foo 对象从未完全构造。
请注意,析构函数体不负责销毁数据成员本身。如果数据成员是对象被销毁时需要释放的资源的句柄(例如文件、套接字、数据库连接、互斥体或堆内存),则只需编写析构函数。
数组元素
数组元素按降序销毁。如果在第n个元素的构造过程中抛出异常,则在传播异常之前将元素n-1到0销毁。
临时对象
在评估类类型的纯右值表达式时会构造一个临时对象。 prvalue 表达式最突出的示例是调用按值返回对象的函数,例如T operator+(const T&, const T&)。在正常情况下,当词法上包含纯右值的完整表达式被完全求值时,临时对象就会被破坏:
__________________________ full-expression
___________ subexpression
_______ subexpression
some_function(a + " " + b);
^ both temporary objects are destructed here
上述函数调用some_function(a + " " + b) 是一个完整表达式,因为它不是更大表达式的一部分(相反,它是表达式语句的一部分)。因此,在评估子表达式期间构造的所有临时对象都将在分号处被破坏。有两个这样的临时对象:第一个是在第一次添加期间构造的,第二个是在第二次添加期间构造的。第二个临时对象将在第一个之前被销毁。
如果在第二次添加过程中抛出异常,则在传播异常之前将正确销毁第一个临时对象。
如果使用纯右值表达式初始化本地引用,则临时对象的生命周期会扩展到本地引用的范围,因此您不会得到悬空引用:
{
const Foo& r = a + " " + b;
^ first temporary (a + " ") is destructed here
// ...
} <--- second temporary (a + " " + b) is destructed not until here
如果非类类型的纯右值表达式被求值,结果是一个值,而不是一个临时对象。但是,如果使用纯右值来初始化引用,则会构造一个临时对象:
const int& r = i + j;
动态对象和数组
在下一节中,destroy X的意思是“先破坏X,然后释放底层内存”。
同理,create X的意思是“先分配足够的内存,然后再在那里构造X”。
动态对象
通过p = new Foo 创建的动态对象通过delete p 销毁。如果您忘记delete p,则您有资源泄漏。您永远不应尝试执行以下操作之一,因为它们都会导致未定义的行为:
- 通过
delete[](注意方括号)、free 或任何其他方式销毁动态对象
- 多次销毁动态对象
- 在动态对象被销毁后访问它
如果在动态对象的构造过程中抛出异常,则在传播异常之前释放底层内存。
(析构函数不会在内存释放之前执行,因为对象从未完全构造。)
动态数组
通过p = new Foo[n] 创建的动态数组通过delete[] p 销毁(注意方括号)。如果您忘记delete[] p,则您有资源泄漏。您永远不应尝试执行以下操作之一,因为它们都会导致未定义的行为:
- 通过
delete、free 或任何其他方式销毁动态数组
- 多次销毁动态数组
- 在动态数组被销毁后访问它
如果在第n个元素的构造过程中抛出异常,则按降序销毁元素n-1到0,释放底层内存,传播异常。
(对于动态数组,您通常应该更喜欢 std::vector<Foo> 而不是 Foo*。它使编写正确且健壮的代码变得更加容易。)
引用计数智能指针
由多个std::shared_ptr<Foo> 对象管理的动态对象在销毁参与共享该动态对象的最后一个std::shared_ptr<Foo> 对象时被销毁。
(对于共享对象,您通常应该更喜欢 std::shared_ptr<Foo> 而不是 Foo*。它使编写正确且健壮的代码变得更加容易。)