析构

https://docs.microsoft.com/en-us/cpp/cpp/destructors-cpp?view=vs-2019

  • 概述

析构操作是一个对象的成员函数,在该对象离开作用域或通过delete操作明确的销毁时,被自动调用。析构操作与类同名,前面加个波浪符 (~)。例如,String类的析构操作的声明是:~String()。

当没有定义析构操作时,编译器会自动生成一个默认的;很多情况下,它是足够的。当类中占用了需要释放的系统资源或者它们拥有的指针占用着内存。

看看如下的一个String类:

// spec1_destructors.cpp
#include <string>

class String {
public:
   String( char *ch );  // Declare constructor
   ~String();           //  and destructor.
private:
   char    *_text;
   size_t  sizeOfText;
};

// Define the constructor.
String::String( char *ch ) {
   sizeOfText = strlen( ch ) + 1;

   // Dynamically allocate the correct amount of memory.
   _text = new char[ sizeOfText ];

   // If the allocation succeeds, copy the initialization string.
   if( _text )
      strcpy_s( _text, sizeOfText, ch );
}

// Define the destructor.
String::~String() {
   // Deallocate the memory that was previously reserved
   //  for this string.
   delete[] _text;
}

int main() {
   String str("The piper in the glen...");
}

在上面的类String中的析构操作 String::~String使用了delete操作符来释放动态分配的内存。

  • 析构操作的声明

析构操作与类同名,前面加个波浪符 (~)。

析构操作的声明有几条基本规则:

  1. 没有参数
  2. 没有返回值
  3. 不能被声明带有限定符 const, volatile, or static。然而,它们可以被带有限定符 const, volatile, or static的对象在析构时调用。
  4. 可以带有virtual。当使用virtual 析构操作时,不需要知道对象的具体类型---该对象对应的正确的析构操作会被自动调用,这是通过virtual 函数机制实现的。说明,在一个抽象类中,析构操作可以被声明成纯虚函数。
  • 使用析构

如下几个事件之一发生时就会调用析构操作:

  1. 局部对象离开作用域
  2. 使用new操作符生成的对象,通过明确使用delete来释放。
  3. 一个临时变量的生命期结束了。
  4. 程序运行完了,全局或静态对象存在。
  5. 使用析构函数的全称来明确调用。

析构操作有两个限制:

  1. 不能获取它的地址
  2. 子类不能继承父类的析构操作
  • 析构操作的调用顺序

当一个对象要被释放时,它的完整的析构操作的事件序列如下:

  1. 类的析构操作被调用,析构函数被执行。
  2. 非静态的成员对象的析构操作被按出现在类声明顺序的相反顺序执行。
  3. 对于“ non-virtual base classes”的析构操作按照声明的反方向顺序执行。
  4. 对于“virtual base classes ”的析构操作按照声明的反方向顺序执行。

举例如下:
// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}

Output: A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

  • 虚拟基类

对于“virtual base classes ”的析构操作按照声明(有向无环图)的反方向顺序执行,规则是“深度优先,从左到右,后序遍历”。下面的图描述了继承关系。

下面显示了声明关系:
class A
class B
class C : virtual public A, virtual public B
class D : virtual public A, virtual public B
class E : public C, public D, virtual public B

为了决定一个E类型的对象的析构操作顺序,编译器通过如下算法建立一个列表:

  1. 遍历图的左侧,从图中最深的点开始,在这个实例中是从E开始.
  2. 执行左侧遍历,直到左侧路径所有节点被访问过。记录当前节点的名称。
  3. 再次访问它的上一个节点(向下或者向右)来确认一下已经被记下的节点是否是一个虚拟基类。
  4. 如果已经被记下的节点是一个虚拟基类,看看list中是否已经存在该节点。如果不是虚拟基类,则忽略。
  5. 如果已经被记下的节点没有在list中,则把它添加到list的底部。
  6. 向图的向上的路径和右侧的路径遍历。
  7. 如果图的向上的路径没有穷尽,则跳转到第2步。
  8. 当最后一条向上的路径被穷尽,记录当前节点名称。
  9. 如果底部节点没有再次出现在当前节点,则跳转到第3步。
  10. 如果底部节点 再次出现在当前节点,则遍历结束。

因此,对于类E,list的排序是:

The virtual base class A.

The virtual base class B.

The non-virtual base class C.

The non-virtual base class D.

The non-virtual base class E.

析构操作顺序为:

The non-virtual base class E.

The non-virtual base class D.

The non-virtual base class C.

The virtual base class B.

The virtual base class A

简单描述一下该list的构造过程。

1)首先定位到点E。

2)根据最左边优先遍历,定位到点A。记录下A节点。

3)A的上一个节点是C不是虚拟基类,忽略。

4)把A记录到list中

5)向C的另一个上侧路径遍历,定位到B。。记录下B节点。

6)B的上一个节点是C不是虚拟基类,忽略。

7)把B记录到list中
8)节点C向上的路径都穷尽了,记录C节点。

9)访问D节点,D不是虚拟基类,忽略。

10)把C记录到list中

10)把D记录到list中

这种在一个继承关系图中存在类之间有相互依赖关系是很危险的。因为后面的子类会改变哪个是最左边路径,因此,它们也会改变构造和析构的顺序。

  • 非virtual基类

The destructors for non-virtual base classes are called in the reverse order in which the base class names are declared. Consider the following class declaration:
class MultInherit : public Base1, public Base2
...
In the preceding example, the destructor for Base2 is called before the destructor for Base1.

  • Explicit destructor calls

Calling a destructor explicitly is seldom necessary. However, it can be useful to perform cleanup of objects placed at absolute addresses. These objects are commonly allocated using a user-defined new operator that takes a placement argument. The delete operator cannot deallocate this memory because it is not allocated from the free store (for more information, see The new and delete Operators). A call to the destructor, however, can perform appropriate cleanup. To explicitly call the destructor for an object, s, of class String, use one of the following statements:
s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call
The notation for explicit calls to destructors, shown in the preceding, can be used regardless of whether the type defines a destructor. This allows you to make such explicit calls without knowing if a destructor is defined for the type. An explicit call to a destructor where none is defined has no effect.

  • Robust programming

A class needs a destructor if it acquires a resource, and to manage the resource safely it probably has to implement a copy constructor and a copy assignment.

If these special functions are not defined by the user, they are implicitly defined by the compiler. The implicitly generated constructors and assignment operators perform shallow, memberwise copy, which is almost certainly wrong if an object is managing a resource.

In the next example, the implicitly generated copy constructor will make the pointers str1.text and str2.text refer to the same memory, and when we return from copy_strings(), that memory will be deleted twice, which is undefined behavior:
void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior
Explicitly defining a destructor, copy constructor, or copy assignment operator prevents implicit definition of the move constructor and the move assignment operator. In this case, failing to provide move operations is usually, if copying is expensive, a missed optimization opportunity.

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值