C++ 中的对象销毁

Posted

技术标签:

【中文标题】C++ 中的对象销毁【英文标题】:Object destruction in C++ 【发布时间】:2011-09-18 04:26:28 【问题描述】:

C++ 中的对象究竟是什么时候销毁的,这意味着什么?由于没有垃圾收集器,我是否必须手动销毁它们?异常如何发挥作用?

(注意:这是Stack Overflow's C++ FAQ 的一个条目。如果您想批评以这种形式提供常见问题解答的想法,那么the posting on meta that started all this 将是这样做的地方。该问题的答案在C++ chatroom,FAQ 想法最初是从这里开始的,所以你的答案很可能会被提出这个想法的人阅读。)

【问题讨论】:

谁为close这个话题投了票?我看不出任何理由。事实上,它肯定是一个很好的常见问题解答。 +1 @Nawaz:但这是一个 好问题(这是 SO 的标准),如果不是由真正需要知道答案的人提出的吗?如果它被问得如此普遍,为什么@Fred 需要自己问这个问题才能提供答案?我的观点很简单,如果你遵守规则,那么“这是一个很好的常见问题解答”并不重要,重要的是“这是一个很好的问题”,而且我至少判断这取决于是否有可能让 OP 得到他需要的答案(在这种情况下这是无效的,因为 OP 知道答案),以及其他有相同问题的人是否有可能找到它。 发布FAQ问题并知道答案的集团是否已经认为这是一个“好的FAQ”是无关紧要的 “如果它被问得这么普遍,为什么@Fred 需要自己问” - 通常是因为不了解这些东西的特定人不会想问,“什么时候是对象被破坏”,相反,他们询问了一些关于他们特定代码的具体问题,答案是“你需要了解你的对象的生命周期”。所以具体问题有太多细节,与同一问题上的其他提问者无关。我不知道这里是否是这种情况,但这是我过去用来向提问者推荐的常见问题解答问题。 @jalf:自我回答问题没有错。事实上,这是值得鼓励的。 【参考方案1】:

在下文中,我将区分作用域对象动态对象动态对象,其确切的销毁时间通常要到运行时才能知道。

虽然类对象的析构语义由析构函数决定,但标量对象的析构始终是空操作。具体来说,破坏指针变量不会破坏指针对象。

作用域对象

自动对象

当控制流离开其定义范围时,自动对象(通常称为“局部变量”)将按照其定义的相反顺序被破坏:

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

本地静态对象

在函数内部定义的静态对象在(如果)控制流第一次通过它们的定义时被构造。1main执行后,它们以相反的顺序被销毁:

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&amp;, const T&amp;)。在正常情况下,当词法上包含纯右值的完整表达式被完全求值时,临时对象就会被破坏:

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

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

如果在第n个元素的构造过程中抛出异常,则按降序销毁元素n-1到0,释放底层内存,传播异常。

(对于动态数组,您通常应该更喜欢 std::vector&lt;Foo&gt; 而不是 Foo*。它使编写正确且健壮的代码变得更加容易。)

引用计数智能指针

由多个std::shared_ptr&lt;Foo&gt; 对象管理的动态对象在销毁参与共享该动态对象的最后一个std::shared_ptr&lt;Foo&gt; 对象时被销毁。

(对于共享对象,您通常应该更喜欢 std::shared_ptr&lt;Foo&gt; 而不是 Foo*。它使编写正确且健壮的代码变得更加容易。)

【讨论】:

没有提到静态局部变量与静态全局变量的销毁顺序 我建议详细描述在非空函数中有自动对象的情况。 @FredOverflow 关于“对于动态数组,您通常应该更喜欢 std::vector&lt;Foo&gt; 而不是 Foo*。” - 实际上,大多数时候std::deque&lt;Foo&gt; 是比std::vector&lt;Foo&gt; 更好的选择,但这是另一个讨论。 @MihaiTodor 我已经看到了很多,但在实践中似乎每个人都使用std::vector而不是std::deque。在这里只为我自己说话,但我喜欢我的记忆是连续的。 @FredOverflow 希望用户在插入元素之前记得resize() :)【参考方案2】:

当对象的生命周期结束并被销毁时,会自动调用对象的析构函数。您通常不应该手动调用它。

我们将以这个对象为例:

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

线程存储持续时间对象

这些对语言来说是新的。它们非常类似于静态存储持续时间对象。但是,它们与应用程序的生命周期不同,与它们关联的执行线程一样长。

【讨论】:

以上是关于C++ 中的对象销毁的主要内容,如果未能解决你的问题,请参考以下文章

C++ 并发销毁

C++ 标准是不是保证 cin、cout 等会先创建后销毁?

C++ 中全局对象销毁和 atexit 之间的顺序

在模块加载/卸载时在外部 C++ 模块中构造/销毁对象

C++内存泄露及常见情况总结

从 .NET 代码中销毁非托管对象