C++中的对象破坏

fre*_*low 67 c++ destructor exception c++-faq object-lifetime

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

(注意:这是Stack Overflow的C++常见问题解答的一个条目.如果你想批评在这种形式下提供常见问题解答的想法,那么发布所有这些的元数据的发布将是这样做的地方.这个问题在C++聊天室中受到监控,其中FAQ的想法一开始就出现了,所以你的答案很可能被那些提出想法的人阅读.)

fre*_*low 84

在下面的文本中,我将区分范围对象,其破坏时间由其封闭范围(函数,块,类,表达式)和动态对象静态确定,其确切的销毁时间通常直到运行时才知道.

虽然类对象的破坏语义是由析构函数决定的,但标量对象的破坏始终是无操作的.具体地说,破坏指针变量并没有破坏指针对象.

Scoped对象

自动对象

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

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
Run Code Online (Sandbox Code Playgroud)

如果在执行函数期间抛出异常,则在异常传播给调用者之前,所有先前构造的自动对象都将被销毁.此过程称为堆栈展开.在堆栈展开期间,没有进一步的例外可能留下前述构造的自动对象的析构函数.否则,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;
Run Code Online (Sandbox Code Playgroud)

请注意,在不同的转换单元中定义的静态对象的构造(和销毁)的相对顺序是不确定的.

如果异常离开静态对象的析构函数,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*
Run Code Online (Sandbox Code Playgroud)

如果异常离开静态对象的析构函数,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
Run Code Online (Sandbox Code Playgroud)

如果在构造其中一个Foo子对象期间抛出异常,则在传播异常之前将破坏其先前构建的所有子对象.在Foo析构函数,而另一方面,将被执行,因为Foo对象是没有完全建立.

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

数组元素

数组元素按降序销毁.如果在构造第n个元素期间抛出异常,则在传播异常之前破坏元素n-1到0.

临时对象

在计算类类型的prvalue表达式时,将构造临时对象.prvalue表达式最突出的例子是调用一个按值返回对象的函数,例如T operator+(const T&, const T&).在正常情况下,当完全评估词法包含prvalue的完整表达式时,将破坏临时对象:

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

上面的函数调用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
Run Code Online (Sandbox Code Playgroud)

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

const int& r = i + j;
Run Code Online (Sandbox Code Playgroud)

动态对象和数组

在下一节中,destroy X表示"首先破坏X然后释放底层内存".类似地,创建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*共享对象,这使得编写正确的,健壮的代码要容易得多.)


Mar*_*ork 35

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

我们将使用此对象作为示例:

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";}
};
Run Code Online (Sandbox Code Playgroud)

C++中有三种(C++ 11中有四种)不同类型的对象,对象的类型定义了对象的生命周期.

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

静态存储持续时间对象

这些是最简单的,等同于全局变量.这些对象的生命周期(通常)是应用程序的长度.这些(通常)是在我们退出main之后在main输入和销毁之前(以创建的相反顺序)构造的.

Test  global;
int main()
{
    std::cout << "Main\n";
}

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0
Run Code Online (Sandbox Code Playgroud)

注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
Run Code Online (Sandbox Code Playgroud)

成员变量

成员变量的生命周期绑定到拥有它的对象.当业主的寿命结束时,其所有成员的寿命也将结束.因此,您需要查看遵守相同规则的所有者的生命周期.

注意:成员始终以所有者的相反顺序销毁.

  • 因此,对于类成员,它们按声明的顺序创建,并以与声明
    相反的顺序销毁
  • 因此,对于数组成员,它们按顺序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 ';'
Run Code Online (Sandbox Code Playgroud)

注意:有些情况下可以延长临时寿命.
但这与这个简单的讨论无关.当你明白这个文件对你来说是第二天性之前,在延长临时生命之前你不想做什么.

动态存储持续时间对象

这些对象具有动态生命周期,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
Run Code Online (Sandbox Code Playgroud)

对于来自垃圾收集语言的开发人员来说,这看起来很奇怪(管理对象的生命周期).但问题并不像看起来那么糟糕.在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
Run Code Online (Sandbox Code Playgroud)

线程存储持续时间对象

这些是该语言的新功能.它们非常类似于静态存储持续时间对象.但是,与他们所生活的应用程序生活相同的生命,只要与它们相关联的执行线程.