异常-C ++中的对象销毁

在C ++中确切地销毁对象是什么意思? 因为没有垃圾收集器,我是否必须手动销毁它们? 异常如何发挥作用?

(注意:这是Stack Overflow的C ++ FAQ的一个条目。如果您想批评以这种形式提供FAQ的想法,则可以在开始所有这些操作的meta上进行发布。) 该问题在C ++聊天室中进行监控,C ++聊天室首先出现了FAQ想法,因此,提出这个想法的人很可能会读懂您的答案。)

2个解决方案
76 votes

在下文中,我将区分作用域对象和动态对象,这些对象的销毁时间由其封闭范围(函数,块,类,表达式)静态确定,而动态对象的确切销毁时间通常要到运行时才能知道。

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

范围对象

自动物体

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

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

破坏者永远不应该抛出。

非本地静态对象

在执行std::shared_ptr<Foo>后,将按照其定义的相反顺序销毁在名称空间范围内定义的静态对象(通常称为“全局变量”)和静态数据成员:

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::shared_ptr<Foo>

局部静态对象

函数内部定义的静态对象是在(以及是否)控制流首次通过其定义时构造的。1在执行std::shared_ptr<Foo>后,它们以相反的顺序被破坏:

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::shared_ptr<Foo>

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

如果在std::shared_ptr<Foo>的子对象之一的构造过程中引发了异常,则在传播该异常之前,将破坏其所有先前构造的子对象。 另一方面,将不会执行Foo*析构函数,因为delete[] p对象从未完全构造。

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

数组元素

数组元素按降序破坏。 如果在第n个元素的构造过程中引发异常,则在传播异常之前会破坏元素n-1至0。

临时对象

当评估类类型的prvalue表达式时,将构造一个临时对象。 prvalue表达式最突出的示例是调用按值返回对象的函数,例如std::shared_ptr<Foo>。在正常情况下,当完全评估词法包含prvalue的完整表达式时,将破坏临时对象:

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

上面的函数调用std::shared_ptr<Foo>是完整表达式,因为它不是较大表达式的一部分(相反,它是表达式语句的一部分)。 因此,在子表达式的评估期间构造的所有临时对象都将在分号处被破坏。 有两个这样的临时对象:第一个是在第一次添加时构造的,第二个是在第二次添加期间构造的。 第二个临时对象将在第一个临时对象之前被销毁。

如果在第二次添加过程中引发了异常,则在传播该异常之前,将适当地破坏第一个临时对象。

如果使用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然后释放底层内存”。同样,创建X意味着“首先分配足够的内存,然后在此处构造X”。

动态物体

通过std::shared_ptr<Foo>创建的动态对象将通过Foo*销毁。如果忘记了delete[] p,则会发生资源泄漏。 您永远不要尝试执行以下操作之一,因为它们都会导致未定义的行为:

  • 通过std::shared_ptr<Foo>(请注意方括号),Foo*或其他任何方式销毁动态对象
  • 多次破坏动态对象
  • 在动态对象被销毁后访问它

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

动态数组

通过std::shared_ptr<Foo>创建的动态数组将通过Foo*销毁(请注意方括号)。 如果您忘记了delete[] p,则有资源泄漏。 您永远不要尝试执行以下操作之一,因为它们都会导致未定义的行为:

  • 通过std::shared_ptr<Foo>Foo*或任何其他方式销毁动态数组
  • 多次破坏动态数组
  • 在动态数组被销毁后访问它

如果在第n个元素的构造过程中引发异常,则将元素n-1到0降序破坏,释放基础内存,并传播该异常。

(对于动态数组,通常应比Foo*更喜欢std::shared_ptr<Foo>。它使编写正确而健壮的代码更加容易。)

引用计数智能指针

由数个std::shared_ptr<Foo>对象管理的动态对象在共享该动态对象所涉及的最后Foo*对象的破坏期间被破坏。

(与共享对象Foo*相比,您通常应该优先选择std::shared_ptr<Foo>,而不是Foo*。这使编写正确而健壮的代码更加容易。)

fredoverflow answered 2019-10-04T07:54:56Z
34 votes

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

我们将以该对象为例:

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 ';'

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

动态存储持续时间对象

这些对象具有动态寿命,并使用std::shared_ptr创建,并通过调用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

线程存储持续时间对象

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

Martin York answered 2019-10-04T07:59:59Z
translate from https://stackoverflow.com:/questions/6403055/object-destruction-in-c