C++ 中的对象销毁

Object destruction in C++

提问人:fredoverflow 提问时间:6/19/2011 最后编辑:Dave Newtonfredoverflow 更新时间:8/10/2021 访问量:41662

问:

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

(注意:这是Stack Overflow的C++ FAQ的条目。如果你想批评以这种形式提供常见问题解答的想法,那么在开始这一切的 meta 上的帖子将是这样做的地方。该问题的答案在 C++ 聊天室中受到监控,FAQ 的想法最初是从那里开始的,所以你的答案很可能会被提出这个想法的人阅读。

C 异常 析构函数 C++-FAQ 对象生存期

评论

9赞 Nawaz 6/19/2011
谁投票支持关闭这个话题?我看不出有什么理由。事实上,这肯定是一个很好的常见问题解答。+1
3赞 jalf 6/19/2011
@Nawaz:但是,如果这不是一个真正需要知道答案的人提出的问题(这是SO的标准),那么这是一个好问题吗?如果这个问题经常被问到,为什么@Fred需要自己问这个问题,这样他才能提供答案?我的观点很简单,如果你遵守规则,那么“这是一个很好的常见问题解答”并不重要,重要的是“这是一个好问题吗”,我至少根据它是否有可能让 OP 得到他需要的答案来判断这一点(在这种情况下是无效的,因为 OP 知道答案), 以及是否有可能发现其他有同样问题的人。
3赞 jalf 6/19/2011
发布FAQ问题并知道答案的集团是否已经认为这是一个“好的FAQ”是无关紧要的
9赞 Steve Jessop 6/20/2011
“如果这个问题被问得如此普遍,为什么@Fred需要自己问”——通常是因为不了解这些东西的特定人不会想问“对象何时被销毁”,而是他们会问一些关于其特定代码的特定问题,答案是,“你需要了解你的对象的生命周期”。因此,具体问题有太多的细节,与同一问题上的其他提问者无关。我不知道这里是否是这种情况,但这是我过去用来推荐提问者的常见问题解答问题。
4赞 Andreas Bonini 6/20/2011
@jalf:自我回答问题没有错。事实上,这是值得鼓励的。

答:

101赞 fredoverflow 6/19/2011 #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

本地静态对象

函数内部定义的静态对象是在(以及如果)控制流首次通过其定义时构造的。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

如果在构造其中一个子对象期间抛出异常,则在传播异常之前,将销毁其先前构造的所有子对象。另一方面,析构函数不会被执行,因为对象从未被完全构造。FooFooFoo

请注意,析构函数正文不负责拆解数据成员本身。仅当数据成员是对象被析构时需要释放的资源(例如文件、套接字、数据库连接、互斥锁或堆内存)的句柄时,才需要编写析构函数。

数组元素

数组元素按降序进行分解。如果在构造第 n 个元素期间引发异常,则在传播异常之前会破坏元素 n-1 到 0。

临时对象

在计算类类型的 prvalue 表达式时构造一个临时对象。prvalue 表达式最突出的例子是调用按值返回对象的函数,例如 .在正常情况下,当完全计算词法上包含 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)

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

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

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

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

const int& r = i + j;

动态对象和数组

在下一节中,destroy X 的意思是“先销毁 X,然后释放底层内存”。 同样,create X 的意思是“首先分配足够的内存,然后在那里构造 X”。

动态对象

通过 创建的动态对象通过 销毁。如果忘记了,则存在资源泄漏。切勿尝试执行以下操作之一,因为它们都会导致未定义的行为:p = new Foodelete pdelete p

  • 通过(注意方括号)或任何其他方式销毁动态对象delete[]free
  • 多次销毁动态对象
  • 在动态对象被销毁后对其进行访问

如果在构造动态对象期间引发异常,则在传播异常之前会释放基础内存。 (析构函数不会在内存释放之前执行,因为该对象从未完全构造过。

动态数组

通过创建的动态数组被销毁(注意方括号)。如果忘记了,则存在资源泄漏。切勿尝试执行以下操作之一,因为它们都会导致未定义的行为:p = new Foo[n]delete[] pdelete[] p

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

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

(通常应优先选择动态数组。它使编写正确和健壮的代码变得更加容易。std::vector<Foo>Foo*

引用计数智能指针

由多个对象管理的动态对象在销毁共享该动态对象所涉及的最后一个对象时被销毁。std::shared_ptr<Foo>std::shared_ptr<Foo>

(通常应优先选择共享对象。它使编写正确和健壮的代码变得更加容易。std::shared_ptr<Foo>Foo*

评论

0赞 Nick 6/22/2011
没有提到静态局部变量与静态全局变量的销毁顺序
0赞 AerandiR 6/20/2012
我建议详细描述在非 void 函数中有一个自动对象的情况。
0赞 Mihai Todor 7/18/2013
@FredOverflow 关于“您通常应该更喜欢动态数组。 - 实际上,大多数时候是比 更好的选择,但这是另一个讨论。std::vector<Foo>Foo*std::deque<Foo>std::vector<Foo>
0赞 fredoverflow 7/18/2013
@MihaiTodor我看到很多这样的宣讲,但在实践中,似乎每个人都用 .这里只代表我自己,但我喜欢我的记忆是连续的。std::vectorstd::deque
0赞 Mihai Todor 7/18/2013
@FredOverflow 希望用户在插入元素之前会相应地记住它:)resize()
39赞 Martin York 7/30/2012 #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++ 中有四种)不同类型的对象,对象的类型定义了对象的生命周期。

  • 静态存储持续时间对象
  • 自动存储持续时间对象
  • 动态存储持续时间对象
  • (在 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 ';'

注意:在某些情况下,临时的寿命可以延长。
但这与这个简单的讨论无关。当您了解此文档将成为您的第二天性时,并且在它延长临时的寿命之前,您不想这样做。

动态存储持续时间对象

这些对象具有动态生命周期,通过调用 创建和销毁。newdelete

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 收集的语言最接近的是 .这将跟踪动态创建的对象的用户数量,并且当所有用户都消失时将自动调用(我认为这是普通 Java 对象的更好版本)。std::shared_ptrdelete

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

线程存储持续时间对象

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