关于异常:C ++中的对象销毁

Object destruction in C++

什么时候对象在C++中被破坏,这意味着什么?因为没有垃圾收集器,我必须手动销毁它们吗?例外是如何发挥作用的?

(注:这意味着是堆栈溢出的C++FAQ的一个条目。如果你想批评在这个表单中提供一个常见问题解答的想法,那么在meta上发布的开始所有这一切的地方就是这样做的地方。这个问题的答案是在C++聊天室中进行监控的,FAQ的想法一开始就出现了,所以你的答案很可能会被那些想出这个想法的人读到。


在下面的文本中,我将区分作用域对象(其销毁时间由其封闭作用域(函数、块、类、表达式)静态确定)和动态对象(其确切销毁时间通常在运行时才知道)。好的。

虽然类对象的破坏语义由析构函数确定,但标量对象的破坏始终是不可操作的。具体来说,破坏指针变量不会破坏指针。好的。作用域对象自动对象

当控制流离开其定义的范围时,自动对象(通常称为"局部变量")按其定义的相反顺序被销毁:好的。

1
2
3
4
5
6
7
8
9
10
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.

Ok.

非本地静态对象

在执行main之后,在命名空间范围(通常称为"全局变量")定义的静态对象和静态数据成员将按其定义的相反顺序被销毁:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。好的。局部静态对象

在函数内部定义的静态对象是在(和if)控制流首次通过其定义时构造的。1执行main后,它们按相反的顺序销毁:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:这是一个非常简化的模型。静态对象的初始化细节实际上要复杂得多。好的。基类子对象和成员子对象

当控制流离开对象的析构函数体时,其成员子对象(也称为其"数据成员")将按其定义的相反顺序进行析构函数化。之后,它的基类子对象将按与基本说明符列表相反的顺序进行销毁:好的。

1
2
3
4
5
6
7
8
9
10
11
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表达式时,将构造临时对象。prvalue表达式最突出的例子是调用按值返回对象的函数,例如T operator+(const T&, const T&)。在正常情况下,当完全计算词法包含prvalue的完整表达式时,将销毁临时对象:好的。

1
2
3
4
5
__________________________ full-expression
              ___________  subexpression
              _______      subexpression
some_function(a +"" + b);
                          ^ both temporary objects are destructed here

上述函数调用some_function(a +"" + b)是完整表达式,因为它不是较大表达式的一部分(而是表达式语句的一部分)。因此,在子表达式的计算期间构造的所有临时对象都将以分号销毁。有两个这样的临时对象:第一个对象在第一个加法期间构造,第二个对象在第二个加法期间构造。第二个临时对象将在第一个临时对象之前被销毁。好的。

如果在第二次添加期间引发异常,则在传播该异常之前,第一个临时对象将被正确销毁。好的。

如果用prvalue表达式初始化了本地引用,则临时对象的生存期将扩展到本地引用的范围,因此不会得到悬空引用:好的。

1
2
3
4
5
{
    const Foo& r = a +"" + b;
                              ^ first temporary (a +"") is destructed here
    // ...
}  <--- second temporary (a +"" + b) is destructed not until here

如果计算非类类型的prvalue表达式,则结果是一个值,而不是临时对象。但是,如果使用prvalue初始化引用,则将构造一个临时对象:好的。

1
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,你就有了资源泄漏。您不应尝试执行以下操作之一,因为它们都会导致未定义的行为:好的。

  • 通过deletefree或任何其他方式销毁动态阵列
  • 多次销毁动态数组
  • 在动态数组被销毁后访问它

如果在构造第n个元素的过程中引发异常,那么元素n-1到0将按降序销毁,释放底层内存,并传播异常。好的。

(对于动态数组,您通常更喜欢std::vector而不是Foo*。它使编写正确且健壮的代码更加容易。)好的。引用计数智能指针

由多个std::shared_ptr对象管理的动态对象在最后一个涉及共享该动态对象的std::shared_ptr对象的销毁过程中被销毁。好的。

(对于共享对象,您通常更喜欢std::shared_ptr而不是Foo*。它使编写正确且健壮的代码更加容易。)好的。好啊。


对象的析构函数在对象寿命结束并被销毁时自动调用。您通常不应该手动调用它。好的。

我们将以这个对象为例:好的。

1
2
3
4
5
6
7
8
9
10
11
12
class Test
{
    public:
        Test()                           { std::cout <<"Created   " << this <<"
"
;}
        ~Test()                          { std::cout <<"Destroyed " << this <<"
"
;}
        Test(Test const& rhs)            { std::cout <<"Copied    " << this <<"
"
;}
        Test& operator=(Test const& rhs) { std::cout <<"Assigned  " << this <<"
"
;}
};

C++中有三个(C++中有四个)不同类型的对象,对象的类型定义对象的寿命。好的。

  • 静态存储持续时间对象
  • 自动存储持续时间对象
  • 动态存储持续时间对象
  • (在C++ 11中)线程存储持续时间对象

静态存储持续时间对象

这些是最简单的,等于全局变量。这些对象的寿命(通常)是应用程序的长度。这些通常是在进入主系统之前构建的,在退出主系统之后销毁(按照创建的相反顺序)。好的。

1
2
3
4
5
6
7
8
9
10
11
Test  global;
int main()
{
    std::cout <<"Main
"
;
}

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0

注1:静态存储持续时间对象还有两种类型。好的。类的静态成员变量。

这些在所有意义和目的上都与寿命方面的全局变量相同。好的。函数内部的静态变量。

这些是延迟创建的静态存储持续时间对象。它们是在第一次使用时创建的(在C++ 11的线程安全庄园中)。与其他静态存储持续时间对象一样,它们在应用程序结束时被销毁。好的。施工/破坏顺序

  • 编制单位的编制顺序是明确的,与申报顺序相同。
  • 编译单元之间的构造顺序未定义。
  • 破坏的顺序与构造的顺序完全相反。

自动存储持续时间对象

这些是最常见的对象类型,您应该使用99%的时间。好的。

这是三种主要的自动变量:好的。

  • 函数/块内的局部变量
  • 类/数组中的成员变量。
  • 临时变量。

局部变量

当函数/块退出时,该函数/块中声明的所有变量都将被销毁(按创建的相反顺序)。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int main()
{
     std::cout <<"Main() START
"
;
     Test   scope1;
     Test   scope2;
     std::cout <<"Main Variables Created
"
;


     {
           std::cout <<"
block 1 Entered
"
;
           Test blockScope;
           std::cout <<"block 1 about to leave
"
;
     } // blockScope is destrpyed here

     {
           std::cout <<"
block 2 Entered
"
;
           Test blockScope;
           std::cout <<"block 2 about to leave
"
;
     } // blockScope is destrpyed here

     std::cout <<"
Main() END
"
;
}// 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->0

临时变量

这些对象是作为表达式的结果创建的,但不分配给变量。临时变量和其他自动变量一样被销毁。只是它们作用域的结尾是在其中创建它们的语句的结尾(这通常是";")。好的。

1
2
3
4
5
6
7
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销毁。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
    std::cout <<"Main()
"
;
    Test*  ptr = new Test();
    delete ptr;
    std::cout <<"Main Done
"
;
}

> ./a.out
Main()
Created    0x1083008e0
Destroyed  0x1083008e0
Main Done

对于来自垃圾收集语言的开发人员来说,这看起来很奇怪(管理对象的寿命)。但问题并没有看上去那么严重。在C++中,直接使用动态分配的对象是不寻常的。我们有管理对象来控制它们的寿命。好的。

与大多数其他GC收集的语言最接近的是std::shared_ptr。这将跟踪动态创建对象的用户数量,当它们全部消失后,将自动调用EDCOX1×1(我认为这是一个更好的Java对象的版本)。好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
    std::cout <<"Main Start
"
;
    std::shared_ptr<Test>  smartPtr(new Test());
    std::cout <<"Main End
"
;
} // 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

线程存储持续时间对象

这些是新的语言。它们非常类似于静态存储持续时间对象。但与其与应用程序生活在同一个生命周期中,不如与它们关联的执行线程生活在同一个生命周期中。好的。好啊。