在C ++中的对象破坏

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

(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供常见问题的想法,那么开始所有这些的meta上的贴子将成为这样做的地方。那个问题在C ++聊天室中进行监控,常见问题的想法首先出现在C ++聊天室中,所以你的答案很可能会被那些提出这个想法的人阅读。)


在下面的文章中,我将区分作用域对象,它们的销毁时间由它们的封闭作用域(函数,块,类,表达式)静态确定,动态对象的确切时间通常在运行时才知道。

虽然类对象的破坏语义是由析构函数决定的,但标量对象的破坏总是不可操作的。 具体而言,破坏指针变量不会破坏指针。

作用域对象

自动对象

自动对象(通常称为“局部变量”)在控制流离开其定义范围时以与其定义相反的顺序被销毁:

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函数。

本地静态对象

当(和if)控制流第一次通过它们的定义时,函数内部定义的静态对象被构造出来.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:这是一个非常简化的模型。 静态对象的初始化细节实际上要复杂得多。

基类子对象和成员子对象

当控制流离开对象的析构函数体时,其成员子对象(也称为其“数据成员”)按其定义的相反顺序被破坏。 之后,其基类子对象将以与基本说明符列表相反的顺序被破坏:

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表达式时,构造一个临时对象。 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;

动态对象和数组

在下面的章节中,销毁X意味着“先破坏X然后释放底层内存”。 同样,创建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<Foo>超过Foo* ,这使得写入正确和健壮的代码变得更容易。)

    引用计数智能指针

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

    (对于共享对象,您应该更喜欢std::shared_ptr<Foo>而非Foo* ,这使得写入正确且健壮的代码变得更加容易。)


    对象的析构函数在对象使用期限结束并被销毁时自动调用。 你通常不应该手动调用它。

    我们将用这个对象作为例子:

    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之后进入并销毁(按照与创建相反的顺序)之前构建。

    Test  global;
    int main()
    {
        std::cout << "Mainn";
    }
    
    > ./a.out
    Created    0x10fbb80b0
    Main
    Destroyed  0x10fbb80b0
    

    注1:还有两种其他类型的静态存储持续时间对象。

    一个类的静态成员变量。

    就寿命而言,这些用于全部意义和目的与全局变量相同。

    函数内的静态变量。

    这些是懒惰地创建的静态存储持续时间对象。 它们是在第一次使用时创建的(在C ++ 11的线程安全庄园中)。 就像其他静态存储持续时间对象一样,它们在应用程序结束时被销毁

    建造/销毁的顺序

  • 编制单位内的施工顺序已经很好地定义,并且与声明相同。
  • 编译单元之间的构建顺序是不确定的。
  • 破坏的顺序与建造顺序完全相反。
  • 自动存储时间对象

    这些是最常见的对象类型,你应该在99%的时间内使用它。

    这是自动变量的三种主要类型:

  • 函数/块内的局部变量
  • 类/数组中的成员变量。
  • 临时变量。
  • 局部变量

    当一个函数/块退出时,在该函数/块内声明的所有变量都将被销毁(按照与创建相反的顺序)。

    int main()
    {
         std::cout << "Main() STARTn";
         Test   scope1;
         Test   scope2;
         std::cout << "Main Variables Createdn";
    
    
         {
               std::cout << "nblock 1 Enteredn";
               Test blockScope;
               std::cout << "block 1 about to leaven";
         } // blockScope is destrpyed here
    
         {
               std::cout << "nblock 2 Enteredn";
               Test blockScope;
               std::cout << "block 2 about to leaven";
         } // blockScope is destrpyed here
    
         std::cout << "nMain() ENDn";
    }// 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 Donen";
    }
    
    > ./a.out
    Main()
    Created    0x1083008e0
    Destroyed  0x1083008e0
    Main Done
    

    对于来自垃圾收集语言的开发人员来说,这看起来很奇怪(管理对象的使用寿命)。 但问题并不像看起来那么糟糕。 在C ++中直接使用动态分配的对象是不寻常的。 我们有管理对象来控制它们的寿命。

    与大多数其他GC收集的语言最接近的是std::shared_ptr 。 这将跟踪一个动态创建的对象的用户数量,当它们全部消失时会自动调用delete (我认为这是一个普通Java对象的更好的版本)。

    int main()
    {
        std::cout << "Main Startn";
        std::shared_ptr<Test>  smartPtr(new Test());
        std::cout << "Main Endn";
    } // 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
    

    线程存储时间对象

    这些对于该语言来说是新的。 它们非常像静态存储持续时间对象。 但是,只要他们所关联的执行线程与他们生活的应用程序的生活完全相同,就没有什么共同之处。

    链接地址: http://www.djcxy.com/p/12689.html

    上一篇: Object destruction in C++

    下一篇: c++