首页 文章

C中的对象破坏

提问于
浏览
59

究竟是什么物体在C中被摧毁,这意味着什么?我是否必须手动销毁它们,因为没有垃圾收集器?例外是如何发挥作用的?

(注意:这是Stack Overflow的C FAQ的一个条目 . 如果你想批评在这个表单中提供常见问题解答的想法,那么发布所有这些的meta上的帖子就是这样做的地方 . 这个问题在C聊天室中受到监控,其中FAQ的想法首先出现在那里,所以你的答案很可能被那些提出这个想法的人阅读 . )

2 回答

  • 75

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

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

    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
    

    如果在执行函数期间抛出异常,则在异常传播给调用者之前,所有先前构造的自动对象都将被销毁 . 此过程称为堆栈展开 . 在堆栈展开期间,没有进一步的例外可能留下前述构造的自动对象的析构函数 . 否则,调用函数 std::terminate .

    这导致了C中最重要的指导原则之一:

    破坏者不应该扔掉 .

    非本地静态对象

    main 执行后,在命名空间范围内定义的静态对象(通常称为"global variables")和静态数据成员按其定义的相反顺序进行破坏:

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

    __________________________ 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表示"first destruct X and then release the underlying memory" . 同样,创建X表示"first allocate enough memory and then construct X there" .

    动态对象

    通过 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* . 这使得编写正确且健壮的代码变得更加容易 . )

  • 34

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

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

    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 - >顶部顺序创建
      并以相反的顺序销毁 - > 0

    临时变量

    这些是作为表达式结果创建但未分配给变量的对象 . 临时变量就像其他自动变量一样被销毁 . 只是它们的范围的结尾是创建它们的 statement 的结尾(这通常是';') .

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

    线程存储持续时间对象

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

相关问题