在C ++中的对象破坏
什么时候在C ++中被销毁的对象,这是什么意思? 我是否必须手动销毁它们,因为没有垃圾收集器? exception如何发挥作用?
(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。
在下面的文本中,我将区分作用域对象 ,它们的销毁时间是由它们的封闭作用域(函数,块,类,expression式)静态确定的,而dynamic对象的确切时间通常在运行时才知道。
虽然类对象的破坏语义是由析构函数决定的,但标量对象的破坏总是不可操作的。 具体来说,破坏指针variables不会破坏指针。
作用域对象
自动对象
当控制stream程离开定义的范围时,自动对象(通常称为“局部variables”)以与其定义相反的顺序被销毁:
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
如果在函数执行过程中抛出exception,则在将exception传播给调用者之前,所有先前构造的自动对象都被破坏。 这个过程被称为堆栈展开 。 在堆栈展开期间,没有其他例外情况可能留下前述构造的自动对象的析构器。 否则,调用std::terminate
函数。
这导致了C ++中最重要的指导方针之一:
破坏者不应该抛出。
非本地静态对象
在命名空间范围(通常称为“全局variables”)和静态数据成员中定义的静态对象按照其定义的相反顺序在执行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;
请注意,不同翻译单元中定义的静态对象的构造(和销毁)的相对顺序是未定义的。
如果一个exception离开一个静态对象的析构函数,那么调用std::terminate
函数。
本地静态对象
当(和if)控制stream首次通过它们的定义时,函数内定义的静态对象被构造。 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*
如果一个exception离开一个静态对象的析构函数,那么调用std::terminate
函数。
1:这是一个非常简单的模型。 静态对象的初始化细节实际上要复杂得多。
基类子对象和成员子对象
当控制stream离开对象的析构函数体时,其成员子对象(也称为“数据成员”)按其定义的相反顺序被破坏。 之后,其基类子对象将以与基指定者列表相反的顺序被破坏:
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
如果在创build Foo
子对象的过程中抛出一个exception,那么之前构造的子对象将在exception传播之前被破坏。 另一方面, Foo
析构函数不会被执行,因为Foo
对象从来没有被完全构build过。
请注意,析构函数体不负责破坏数据成员本身。 如果数据成员是对象被销毁时(如文件,套接字,数据库连接,互斥锁或堆内存)需要释放的资源的句柄,则只需编写析构函数。
数组元素
数组元素按降序排列。 如果在构造第n个元素的过程中抛出exception,元素n-1到0将在exception传播之前被破坏。
临时对象
当评估类types的prvalueexpression式时,构造一个临时对象。 一个prvalueexpression式的最显着的例子就是函数的调用,它通过值返回一个对象,比如T operator+(const T&, const T&)
。 在正常情况下,当完全评估词法上包含前值的完整expression式时,临时对象被破坏:
__________________________ full-expression ___________ subexpression _______ subexpression some_function(a + " " + b); ^ both temporary objects are destructed here
上面的函数调用some_function(a + " " + b)
是一个完整expression式,因为它不是一个较大expression式的一部分(相反,它是expression式语句的一部分)。 因此,在子expression式评估过程中构造的所有临时对象都将在分号处被破坏。 有两个这样的临时对象:第一个是在第一个加法中构造的,第二个是在第二个加法中构造的。 第二个临时对象将在第一个之前被破坏。
如果在第二次添加期间抛出exception,则第一个临时对象在传播exception之前将被正确地破坏。
如果使用prvalueexpression式初始化本地引用,则临时对象的生命周期将扩展到本地引用的范围,因此您将不会获得悬挂引用:
{ const Foo& r = a + " " + b; ^ first temporary (a + " ") is destructed here // ... } <--- second temporary (a + " " + b) is destructed not until here
如果评估非类types的prvalueexpression式,则结果是一个值 ,而不是临时对象。 但是,如果使用prvalue初始化引用, 则会构造一个临时对象:
const int& r = i + j;
dynamic对象和数组
在下面的章节中, 销毁X的意思是“先破坏X然后释放底层的内存”。 同样, 创buildX意味着“首先分配足够的内存,然后在那里构造X”。
dynamic对象
通过p = new Foo
创build的dynamic对象通过delete p
被销毁。 如果您忘记delete p
,则会发生资源泄漏。 您不应该尝试执行以下任一操作,因为它们都会导致未定义的行为:
- 通过
delete[]
(注意方括号)销毁一个dynamic的对象,free
或任何其他手段 - 摧毁一个dynamic的对象多次
- 在其被销毁后访问dynamic对象
如果在构builddynamic对象期间引发exception,则在传播exception之前释放基础内存。 (析构函数在释放内存之前不会被执行,因为对象从来没有被完全构build。)
dynamic数组
通过p = new Foo[n]
创build的dynamic数组通过delete[] p
(注意方括号)被销毁。 如果您忘记delete[] p
,则说明资源泄漏。 您不应该尝试执行以下任一操作,因为它们都会导致未定义的行为:
- 通过
delete
,free
或任何其他手段破坏dynamic数组 - 摧毁一个dynamic数组多次
- 在其被销毁之后访问dynamic数组
如果在构造第n个元素的过程中抛出exception,元素n-1到0将按降序被析构,底层存储器被释放,exception被传播。
(对于dynamic数组,你通常应该更喜欢std::vector<Foo>
超过Foo*
,这使得写入正确和健壮的代码变得更容易。
引用计数智能指针
由多个std::shared_ptr<Foo>
对象pipe理的dynamic对象在销毁共享该dynamic对象所涉及的最后一个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 ++中有三种不同types的对象(C ++ 11中有四种),对象的types定义了对象的生命周期。
- 静态存储时间对象
- 自动存储持续时间对象
- dynamic存储持续时间对象
- (在C ++ 11中)线程存储持续时间对象
静态存储时间对象
这些是最简单和等同于全局variables。 这些对象的生命周期通常是应用程序的长度。 这些(通常)在main被input并且在我们退出main之后被销毁(按照被创build的相反顺序)被构build。
Test global; int main() { std::cout << "Main\n"; } > ./a.out Created 0x10fbb80b0 Main Destroyed 0x10fbb80b0
注1:还有另外两种types的静态存储持续时间对象。
一个类的静态成员variables。
就寿命而言,这些是全部意义和目的,与全局variables相同。
函数内的静态variables。
这些是懒惰地创build的静态存储持续时间的对象。 它们是在第一次使用时创build的(在C ++ 11中是一个线程安全的庄园)。 就像其他静态存储持续时间对象一样,它们在应用程序结束时被销毁
build造/销毁的顺序
- 编制单位的build设顺序是明确的,同声明一样。
- 编译单元之间的构造顺序是不确定的。
- 破坏顺序与build造顺序完全相反。
自动存储持续时间对象
这些是最常见的对象types,99%的时间应该使用什么types的对象。
这是自动variables的三种主要types:
- 函数/块内的局部variables
- 类/数组中的成员variables。
- 临时variables。
局部variables
当一个函数/块退出时,在该函数/块内声明的所有variables将被销毁(按照与创build相反的顺序)。
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
成员variables
成员variables的生命周期绑定到拥有它的对象。 当业主的寿命终止时,其所有成员的寿命也将结束。 所以你需要看看遵守相同规则的所有者的一生。
注意:成员总是按照与创build相反的顺序销毁。
- 因此,对于class级成员,他们是按照声明的顺序创build的
并以相反的声明顺序销毁 - 因此,对于数组成员,它们按照0 – > top的顺序创build
并以相反的顺序销毁顶部 – > 0
临时variables
这些是作为expression式的结果创build的对象,但未分配给variables。 临时variables像其他自动variables一样被销毁。 只是它们的范围的结束是它们被创build的语句的结束(这通常是';')。
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 ';'
注意:在某些情况下,临时的生命可以延长。
但这与这个简单的讨论无关。 当你明白这个文件对你来说是第二性质的时候,在它延长之前,临时性的生活并不是你想做的事情。
dynamic存储持续时间对象
这些对象有一个dynamic的使用寿命,并创buildnew
和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
对于来自垃圾收集语言的开发人员来说,这看起来很奇怪(pipe理对象的使用寿命)。 但问题并不像看起来那么糟糕。 在C ++中直接使用dynamic分配的对象是不正常的。 我们有pipe理对象来控制他们的寿命。
与大多数其他GC收集语言最接近的是std::shared_ptr
。 这将跟踪一个dynamic创build的对象的用户数量,当他们都不见了会自动调用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
线程存储时间对象
这些对于语言来说是新的。 它们非常像静态存储持续时间对象。 但是,只要他们所关联的执行线程与他们生活的应用程序不同,