C++ 中的对象销毁

发布于 2024-11-16 04:31:35 字数 449 浏览 3 评论 0原文

C++ 中的对象到底何时被销毁,这意味着什么?由于没有垃圾收集器,我是否必须手动销毁它们?例外情况如何发挥作用?

<子><我> (注意:这是Stack Overflow 的 C++ 常见问题的条目。如果您想批评这个想法以这种形式提供常见问题解答,然后开始在元上发布所有这一切都会可以在 C++ 聊天室 中监控该问题的答案,其中包含常见问题解答的想法。首先,所以你的答案很可能会被提出这个想法的人阅读。)

When exactly are objects destroyed in C++, and what does that mean? Do I have to destroy them manually, since there is no Garbage Collector? How do exceptions come into play?


(Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

丢了幸福的猪 2024-11-23 04:31:35

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

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

作用域对象

自动对象

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

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 。

局部静态对象

函数内部定义的静态对象在控制流第一次通过其定义时(且如果)被构造。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 将被破坏。

临时对象

临时对象是在计算类类型的纯右值表达式时构造的。纯右值表达式最突出的示例是调用按值返回对象的函数,例如 T 运算符+(const T&, const T&)。正常情况下,当词法上包含纯右值的完整表达式被完全求值时,临时对象就会被破坏:

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

上面的函数调用 some_function(a + " " + b) 是一个完整表达式,因为它不是更大表达式的一部分(相反,它是表达式语句的一部分)。因此,在子表达式求值期间构造的所有临时对象都将在分号处被破坏。有两个这样的临时对象:第一个是在第一次加法期间构造的,第二个是在第二次加法期间构造的。第二个临时对象将在第一个临时对象之前被销毁。

如果在第二次添加期间引发异常,则在传播异常之前将正确销毁第一个临时对象。

如果使用纯右值表达式初始化本地引用,则临时对象的生命周期将扩展到本地引用的范围,因此您不会获得悬空引用:

{
    const Foo& r = a + " " + b;
                              ^ first temporary (a + " ") is destructed here
    // ...
}  <--- second temporary (a + " " + b) is destructed not until here

如果计算非类类型的纯右值表达式,则result 是一个,而不是临时对象。但是,如果使用纯右值来初始化引用,则将构造一个临时对象:

const int& r = i + j;

动态对象和数组

在下面的部分中,“destroy X”的意思是“首先析构 X,然后释放底层内存”。
类似地,create X 的意思是“首先分配足够的内存,然后在那里构造 X”。

动态对象

通过p = new Foo 创建的动态对象通过delete p 销毁。如果您忘记删除 p,则会发生资源泄漏。您永远不应该尝试执行以下操作之一,因为它们都会导致未定义的行为:

  • 通过 delete[] (注意方括号)、free 或任何其他方式
  • 销毁动态对象多次
  • 访问被销毁的动态对象

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

动态数组

通过 p = new Foo[n] 创建的动态数组是通过 delete[] p 销毁(注意方括号)。如果您忘记delete[] p,则会发生资源泄漏。您永远不应该尝试执行以下操作之一,因为它们都会导致未定义的行为:

  • 通过删除、释放或任何其他方式销毁动态数组
  • 多次销毁动态数组多次
  • 访问已销毁的动态数组

如果构造第n个元素期间抛出异常,则按降序顺序销毁元素n-1到0,并释放底层内存,并且异常被传播。

(对于动态数组,您通常应该更喜欢 std::vector 而不是 Foo*。它使编写正确且健壮的代码变得更加容易。)

引用计数智能

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

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

In the following text, I will distinguish between scoped objects, whose time of destruction is statically determined by their enclosing scope (functions, blocks, classes, expressions), and dynamic objects, whose exact time of destruction is generally not known until runtime.

While the destruction semantics of class objects are determined by destructors, the destruction of a scalar object is always a no-op. Specifically, destructing a pointer variable does not destroy the pointee.

Scoped objects

automatic objects

Automatic objects (commonly referred to as "local variables") are destructed, in reverse order of their definition, when control flow leaves the scope of their definition:

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

If an exception is thrown during the execution of a function, all previously constructed automatic objects are destructed before the exception is propagated to the caller. This process is called stack unwinding. During stack unwinding, no further exceptions may leave the destructors of the aforementioned previously constructed automatic objects. Otherwise, the function std::terminate is called.

This leads to one of the most important guidelines in C++:

Destructors should never throw.

non-local static objects

Static objects defined at namespace scope (commonly referred to as "global variables") and static data members are destructed, in reverse order of their definition, after the execution of 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;

Note that the relative order of construction (and destruction) of static objects defined in different translation units is undefined.

If an exception leaves the destructor of a static object, the function std::terminate is called.

local static objects

Static objects defined inside functions are constructed when (and if) control flow passes through their definition for the first time.1
They are destructed in reverse order after the execution of 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*

If an exception leaves the destructor of a static object, the function std::terminate is called.

1: This is an extremely simplified model. The initialization details of static objects are actually much more complicated.

base class subobjects and member subobjects

When control flow leaves the destructor body of an object, its member subobjects (also known as its "data members") are destructed in reverse order of their definition. After that, its base class subobjects are destructed in reverse order of the 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

If an exception is thrown during the construction of one of Foo's subobjects, then all its previously constructed subobjects will be destructed before the exception is propagated. The Foo destructor, on the other hand, will not be executed, since the Foo object was never fully constructed.

Note that the destructor body is not responsible for destructing the data members themselves. You only need to write a destructor if a data member is a handle to a resource that needs to be released when the object is destructed (such as a file, a socket, a database connection, a mutex, or heap memory).

array elements

Array elements are destructed in descending order. If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed before the exception is propagated.

temporary objects

A temporary object is constructed when a prvalue expression of class type is evaluated. The most prominent example of a prvalue expression is the call of a function that returns an object by value, such as T operator+(const T&, const T&). Under normal circumstances, the temporary object is destructed when the full-expression that lexically contains the prvalue is completely evaluated:

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

The above function call some_function(a + " " + b) is a full-expression because it is not part of a larger expression (instead, it is part of an expression-statement). Hence, all temporary objects that are constructed during the evaluation of the subexpressions will be destructed at the semicolon. There are two such temporary objects: the first is constructed during the first addition, and the second is constructed during the second addition. The second temporary object will be destructed before the first.

If an exception is thrown during the second addition, the first temporary object will be destructed properly before propagating the exception.

If a local reference is initialized with a prvalue expression, the lifetime of the temporary object is extended to the scope of the local reference, so you won't get a dangling reference:

{
    const Foo& r = a + " " + b;
                              ^ first temporary (a + " ") is destructed here
    // ...
}  <--- second temporary (a + " " + b) is destructed not until here

If a prvalue expression of non-class type is evaluated, the result is a value, not a temporary object. However, a temporary object will be constructed if the prvalue is used to initialize a reference:

const int& r = i + j;

Dynamic objects and arrays

In the following section, destroy X means "first destruct X and then release the underlying memory".
Similarly, create X means "first allocate enough memory and then construct X there".

dynamic objects

A dynamic object created via p = new Foo is destroyed via delete p. If you forget to delete p, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:

  • destroy a dynamic object via delete[] (note the square brackets), free or any other means
  • destroy a dynamic object multiple times
  • access a dynamic object after it has been destroyed

If an exception is thrown during the construction of a dynamic object, the underlying memory is released before the exception is propagated.
(The destructor will not be executed prior to memory release, because the object was never fully constructed.)

dynamic arrays

A dynamic array created via p = new Foo[n] is destroyed via delete[] p (note the square brackets). If you forget to delete[] p, you have a resource leak. You should never attempt to do one of the following, since they all lead to undefined behavior:

  • destroy a dynamic array via delete, free or any other means
  • destroy a dynamic array multiple times
  • access a dynamic array after it has been destroyed

If an exception is thrown during the construction of the n-th element, the elements n-1 to 0 are destructed in descending order, the underlying memory is released, and the exception is propagated.

(You should generally prefer std::vector<Foo> over Foo* for dynamic arrays. It makes writing correct and robust code much easier.)

reference-counting smart pointers

A dynamic object managed by several std::shared_ptr<Foo> objects is destroyed during the destruction of the last std::shared_ptr<Foo> object involved in sharing that dynamic object.

(You should generally prefer std::shared_ptr<Foo> over Foo* for shared objects. It makes writing correct and robust code much easier.)

隐诗 2024-11-23 04:31: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";}
};

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

注意:在某些情况下可以延长临时的寿命。
但这与这个简单的讨论无关。当你明白这份文件将成为你的第二天性时,在它延长临时文件的寿命之前,你就不想做这件事了。

动态存储持续时间对象

这些对象具有动态生命周期,通过 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

线程存储持续时间对象

这些对于该语言来说是新的。它们非常类似于静态存储持续时间对象。但它们并不是与应用程序拥有相同的生命周期,而是与它们关联的执行线程一样长。

The destructor of an object is called automatically when the object lifespan ends and it is destroyed. You should not usually call it manually.

We will use this object as an example:

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";}
};

There are three (four in C++11) distinct types of object in C++ and the type of the object defines the objects lifespan.

  • Static Storage duration objects
  • Automatic Storage duration objects
  • Dynamic Storage duration objects
  • (In C++11) Thread Storage duration objects

Static Storage duration objects

These are the simplest and equate to global variables. The lifespan of these objects is (usually) the length of the application. These are (usually) constructed before main is entered and destroyed (in the reverse order of being created) after we exit main.

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

> ./a.out
Created    0x10fbb80b0
Main
Destroyed  0x10fbb80b0

Note 1: There are two other type of static storage duration object.

static member variables of a class.

These are for all sense and purpose the same as global variables in terms of lifespan.

static variables inside a function.

These are lazily created static storage duration objects. They are created on first use (in a thread safe manor for C++11). Just like other static storage duration objects they are destroyed when the application ends.

Order of construction/destruction

  • The order of construction within a compilation unit is well defined and the same as declaration.
  • The order of construction between compilation units is undefined.
  • The order of destruction is the exact inverse of the order of construction.

Automatic Storage duration objects

These are the most common type of objects and what you should be using 99% of the time.

These are three main types of automatic variables:

  • local variables inside a function/block
  • member variables inside a class/array.
  • temporary variables.

Local Variables

When a function/block is exited all variables declared inside that function/block will be destroyed (in the reverse order of creation).

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

member variables

The lifespan of a member variables is bound to the object that owns it. When an owners lifespan ends all its members lifespan also ends. So you need to look at the lifetime of an owner which obeys the same rules.

Note: Members are always destroyed before the owner in reverse order of creation.

  • Thus for class members they are created in the order of declaration
    and destroyed in the reverse order of declaration
  • Thus for array members they are created in order 0-->top
    and destroyed in the reverse order top-->0

temporary variables

These are objects that are created as the result of an expression but are not assigned to a variable. Temporary variables are destroyed just like other automatic variables. It is just that the end of their scope is the end of the statement in which they are created (this is usally the ';').

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

Note: There are situations where the life of a temporary can be extended.
But this is not relevant to this simple discussion. By the time you understand that this document will be second nature to you and before it is extending the life of a temporary is not something you want to do.

Dynamic Storage duration objects

These objects have a dynamic lifespan and are created with new and destroyed with a call to 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

For devs that come from garbage collected languages this can seem strange (managing the lifespan of your object). But the problem is not as bad as it seems. It is unusual in C++ to use dynamically allocated objects directly. We have management objects to control their lifespan.

The closest thing to most other GC collected languages is the std::shared_ptr. This will keep track of the number of users of a dynamically created object and when all of them are gone will call delete automatically (I think of this as a better version of a normal Java object).

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

Thread Storage duration objects

These are new to the language. They are very much like static storage duration objects. But rather than living the same life as the application they live as long as the thread of execution they are associated with.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文