C++ 中的对象究竟何时被销毁,这意味着什么?我是否必须手动销毁它们,因为没有垃圾收集器?异常如何发挥作用?
(注意:这是对 Stack Overflow's C++ FAQ 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么 the posting on meta that started all this 将是这样做的地方。该问题的答案在C++ chatroom ,FAQ 的想法首先从这里开始,因此您的答案很可能会被提出该想法的人阅读。)
最佳答案
在下面的文本中,我将区分作用域对象,其销毁时间由其封闭范围(函数、块、类、表达式)静态确定,以及动态对象,其确切的销毁时间通常直到运行时才知道。
虽然类对象的销毁语义由析构函数决定,但标量对象的销毁始终是空操作。具体来说,销毁指针变量不会销毁指针对象。
作用域对象
自动对象
当控制流离开其定义的范围时,自动对象(通常称为“局部变量”)将按照其定义的相反顺序进行销毁:
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++ 中最重要的准则之一:
Destructors should never throw.
非本地静态对象
在命名空间范围内定义的静态对象(通常称为“全局变量”)和静态数据成员在执行
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*
。它使编写正确和健壮的代码变得更加容易。)
关于c++ - C++中的对象销毁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6403055/