C中的对象破坏

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

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

回答(2)

2 years ago

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

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

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

2 years ago

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

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

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

线程存储持续时间对象

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