c++ - C++中的对象销毁

标签 c++ exception destructor c++-faq object-lifetime

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++ 中最重要的准则之一:

Destructors should never throw.



非本地静态对象

在命名空间范围内定义的静态对象(通常称为“全局变量”)和静态数据成员在执行 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

如果在 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[] 销毁动态对象(注意方括号),free或任何其他方式
  • 多次销毁动态对象
  • 访问已销毁的动态对象

  • 如果在构建动态对象的过程中抛出异常,则在传播异常之前释放底层内存。
    (析构函数不会在内存释放之前执行,因为对象从未完全构造过。)

    动态数组

    通过 p = new Foo[n] 创建的动态数组通过 delete[] p 销毁(注意方括号)。如果您忘记了 delete[] p ,你有资源泄漏。您永远不应该尝试执行以下操作之一,因为它们都会导致未定义的行为:
  • 通过 delete 销毁动态数组, free或任何其他方式
  • 多次销毁动态数组
  • 访问已被销毁的动态数组

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

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

    引用计数智能指针

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

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

    关于c++ - C++中的对象销毁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6403055/

    相关文章:

    c++ - 在 c++ () 或 {} 中委托(delegate)构造函数

    scala - 当我发现 Scala 中的数据状态不一致时,应抛出哪个异常?

    c++ - 类的析构函数是否自动为 char * 类型的数据成员释放内存? C++

    java - 字符串的 NullPointerException

    python - 构造函数和析构函数如何工作?

    c++ - 为什么C++偏向析构函数异常?

    c++ - sleep () 与 sleep_for()

    c++ - 检测字符串中的 Unicode 字符

    c++ - 如何在不使用 argv 和 argc 的情况下将总和值传递给 C++ 程序

    c# - 为什么不抛出这个异常?