究竟什么时候在 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++ 中最重要的准则之一:
析构函数永远不应该抛出。
非局部静态对象
在命名空间范围内定义的静态对象(通常称为“全局变量”)和静态数据成员在 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:这是一个极其简化的模型。静态对象的初始化细节实际上要复杂得多。
基类子对象和成员子对象
当控制流离开对象的析构函数体时,其成员子对象(也称为“数据成员”)按其定义的相反顺序被销毁。之后,它的基类子对象以与基说明符列表相反的顺序被破坏:
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[](注意方括号)、免费或任何其他方式销毁动态对象
多次销毁动态对象
在动态对象被销毁后访问它
如果在动态对象的构造过程中抛出异常,则在传播异常之前释放底层内存。 (析构函数不会在内存释放之前执行,因为对象从未完全构造。)
动态数组
通过 p = new Foo[n]
创建的动态数组通过 delete[] p
销毁(注意方括号)。如果您忘记了 delete[] p
,则您有资源泄漏。您永远不应尝试执行以下操作之一,因为它们都会导致未定义的行为:
通过删除、释放或任何其他方式销毁动态数组
多次销毁动态数组
在动态数组被销毁后访问它
如果在第n个元素的构造过程中抛出异常,则按降序销毁元素n-1到0,释放底层内存,传播异常。
(对于动态数组,您通常应该更喜欢 std::vector<Foo>
而不是 Foo*
。它使编写正确且健壮的代码变得更加容易。)
引用计数智能指针
由多个 std::shared_ptr<Foo>
对象管理的动态对象在销毁参与共享该动态对象的最后一个 std::shared_ptr<Foo>
对象期间被销毁。
(对于共享对象,您通常应该更喜欢 std::shared_ptr<Foo>
而不是 Foo*
。它使编写正确和健壮的代码变得更加容易。)
当对象的生命周期结束并被销毁时,会自动调用对象的析构函数。您通常不应该手动调用它。
我们将以这个对象为例:
class Test
{
public:
Test() { std::cout << "Created " << this << "\n";}
~Test() { std::cout << "Destroyed " << this << "\n";}
Test(Test const& rhs) { std::cout << "Copied " << this << "\n";}
Test& operator=(Test const& rhs) { std::cout << "Assigned " << this << "\n";}
};
C++ 中有三种(C++11 中四种)不同类型的对象,对象的类型定义了对象的生命周期。
静态存储持续时间对象
自动存储持续时间对象
动态存储持续时间对象
(在 C++11 中)线程存储持续时间对象
静态存储持续时间对象
这些是最简单的,等同于全局变量。这些对象的生命周期(通常)是应用程序的长度。这些(通常)在进入 main 之前构建,并在我们退出 main 之后销毁(以创建的相反顺序)。
Test global;
int main()
{
std::cout << "Main\n";
}
> ./a.out
Created 0x10fbb80b0
Main
Destroyed 0x10fbb80b0
注 1:还有另外两种类型的静态存储持续时间对象。
类的静态成员变量。
在所有意义上和目的上,这些在寿命方面与全局变量相同。
函数内部的静态变量。
这些是延迟创建的静态存储持续时间对象。它们是在首次使用时创建的(在 C++11 的线程安全庄园中)。就像其他静态存储持续时间对象一样,它们在应用程序结束时被销毁。
建造/销毁顺序
编译单元内的构造顺序定义明确,与声明相同。
编译单元之间的构造顺序是未定义的。
破坏的顺序与构造的顺序正好相反。
自动存储持续时间对象
这些是最常见的对象类型,并且您应该在 99% 的时间里使用它们。
以下是三种主要类型的自动变量:
函数/块内的局部变量
类/数组中的成员变量。
临时变量。
局部变量
当一个函数/块退出时,该函数/块内声明的所有变量都将被销毁(以创建的相反顺序)。
int main()
{
std::cout << "Main() START\n";
Test scope1;
Test scope2;
std::cout << "Main Variables Created\n";
{
std::cout << "\nblock 1 Entered\n";
Test blockScope;
std::cout << "block 1 about to leave\n";
} // blockScope is destrpyed here
{
std::cout << "\nblock 2 Entered\n";
Test blockScope;
std::cout << "block 2 about to leave\n";
} // blockScope is destrpyed here
std::cout << "\nMain() END\n";
}// All variables from main destroyed here.
> ./a.out
Main() START
Created 0x7fff6488d938
Created 0x7fff6488d930
Main Variables Created
block 1 Entered
Created 0x7fff6488d928
block 1 about to leave
Destroyed 0x7fff6488d928
block 2 Entered
Created 0x7fff6488d918
block 2 about to leave
Destroyed 0x7fff6488d918
Main() END
Destroyed 0x7fff6488d930
Destroyed 0x7fff6488d938
成员变量
成员变量的生命周期与拥有它的对象绑定。当所有者的生命周期结束时,其所有成员的生命周期也会结束。因此,您需要查看遵守相同规则的所有者的生命周期。
注意:成员总是在所有者之前以相反的创建顺序被销毁。
因此,对于类成员,它们按照声明的顺序创建并按照声明的相反顺序销毁
因此,对于数组成员,它们按 0-->top 的顺序创建并以相反的顺序 top-->0 销毁
临时变量
这些是作为表达式的结果创建但未分配给变量的对象。临时变量像其他自动变量一样被销毁。只是它们范围的结尾是创建它们的语句的结尾(这通常是';')。
std::string data("Text.");
std::cout << (data + 1); // Here we create a temporary object.
// Which is a std::string with '1' added to "Text."
// This object is streamed to the output
// Once the statement has finished it is destroyed.
// So the temporary no longer exists after the ';'
注意:在某些情况下可以延长临时工的寿命。但这与这个简单的讨论无关。当您了解本文档将成为您的第二天性并且在它延长临时生命之前不是您想要做的事情时。
动态存储持续时间对象
这些对象具有动态生命周期,并使用 new
创建并通过调用 delete
销毁。
int main()
{
std::cout << "Main()\n";
Test* ptr = new Test();
delete ptr;
std::cout << "Main Done\n";
}
> ./a.out
Main()
Created 0x1083008e0
Destroyed 0x1083008e0
Main Done
对于来自垃圾收集语言的开发人员来说,这可能看起来很奇怪(管理对象的生命周期)。但问题并不像看起来那么糟糕。在 C++ 中直接使用动态分配的对象是不常见的。我们有管理对象来控制它们的寿命。
与大多数其他 GC 收集的语言最接近的是 std::shared_ptr
。这将跟踪动态创建的对象的用户数量,当所有用户都消失时,将自动调用 delete
(我认为这是普通 Java 对象的更好版本)。
int main()
{
std::cout << "Main Start\n";
std::shared_ptr<Test> smartPtr(new Test());
std::cout << "Main End\n";
} // smartPtr goes out of scope here.
// As there are no other copies it will automatically call delete on the object
// it is holding.
> ./a.out
Main Start
Created 0x1083008e0
Main Ended
Destroyed 0x1083008e0
线程存储持续时间对象
这些对语言来说是新的。它们非常类似于静态存储持续时间对象。但是,它们不会与应用程序过着相同的生活,而是与它们关联的执行线程一样长。
std::vector<Foo>
而不是Foo*
。” - 实际上,大多数时候std::deque<Foo>
是比std::vector<Foo>
更好的选择,但这是另一个讨论。std::vector
而不是std::deque
。在这里只为我自己说话,但我喜欢我的记忆是连续的。resize()
:)