全面解析C++中的析构函数

“析构函数”是构造函数的反向函数。在销毁(释放)对象时将调用它们。通过在类名前面放置一个波形符 (~) 将函数指定为类的析构函数。例如,声明 String 类的析构函数:~String()。
在 /clr 编译中,析构函数在释放托管和非托管资源方面发挥了特殊作用。
析构函数通常用于在不再需要某个对象时“清理”此对象。请考虑 String 类的以下声明:

// spec1_destructors.cpp
#include <string.h>

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.
  if (_text)
   delete[] _text;
}

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

在前面的示例中,析构函数 String::~String 使用 delete 运算符来动态释放为文本存储分配的空间。
声明析构函数
析构函数是具有与类相同的名称但前面是波形符 (~) 的函数
该语法的第一种形式用于在类声明中声明或定义的析构函数;第二种形式用于在类声明的外部定义的析构函数。
多个规则管理析构函数的声明。析构函数:

  • 不接受参数。
  • 无法指定任何返回类型(包括 void)。
  • 无法使用 return 语句返回值。
  • 无法声明为 const、volatile 或 static。但是,可以为声明为 const、volatile 或 static 的对象的析构调用它们。
  • 可以声明为 virtual。通过使用虚拟析构函数,无需知道对象的类型即可销毁对象 - 使用虚函数机制调用该对象的正确析构函数。请注意,析构函数也可以声明为抽象类的纯虚函数。

使用构造函数

当下列事件之一发生时,将调用析构函数:
使用 delete 运算符显式解除分配了使用 new 运算符分配的对象。使用 delete 运算符解除分配对象时,将为“大多数派生对象”或为属于完整对象,但不是表示基类的子对象的对象释放内存。此“大多数派生对象”解除分配一定仅对虚拟析构函数有效。在类型信息与实际对象的基础类型不匹配的多重继承情况下,取消分配可能失败。
具有块范围的本地(自动)对象超出范围。
临时对象的生存期结束。
程序结束,并且存在全局或静态对象。
使用析构函数的完全限定名显式调用了析构函数。(有关详细信息,请参阅显式析构函数调用。)
前面的列表中所述的情况将确保所有对象均可通过用户定义的方法进行销毁。
如果基类或数据成员有一个可访问的析构函数,并且派生类未声明析构函数,则编译器将生成一个析构函数。此编译器生成的析构函数将为派生类型的成员调用基类析构函数和析构函数。默认析构函数是公共的。

析构函数可以随意调用类成员函数和访问类成员数据。从析构函数调用虚函数时,调用的函数是当前正在销毁的类的函数。
析构的顺序
当对象超出范围或被删除时,其完整析构中的事件序列如下所示:
将调用该类的析构函数,并且会执行该析构函数的主体。
按照非静态成员对象的析构函数在类声明中的显示顺序的相反顺序调用这些函数。用于这些成员的构造的可选成员优化列表不影响构造或析构的顺序。
非虚拟基类的析构函数以声明的相反顺序被调用。
虚拟基类的析构函数以声明的相反顺序被调用。

// order_of_destruction.cpp
#include <stdio.h>

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

输出:

A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

虚拟基类
按照与虚拟基类在定向非循环图形中显示的顺序的相反顺序调用这些虚拟基类的析构函数(深度优先、从左到右、后序遍历)。下图描述了继承关系图。

演示虚拟基类的继承关系图
下面列出了图中显示的类的类头。

  • 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. 如果记住的节点是虚拟基类,请浏览列表以查看是否已将其输入。如果它不是虚拟基类,则将其忽略。
  5. 如果记住的节点尚未包含在列表中,请将其添加到列表的底部。
  6. 向上遍历关系图并沿下一个路径向右遍历。
  7. 转到步骤 2。
  8. 在用完最后一个向上路径时,请记下当前节点的名称。
  9. 转到步骤 3。
  10. 继续执行此过程,直到底部节点再次成为当前节点。

因此,对于 E 类,析构顺序为:

  1. 非虚拟基类 E。
  2. 非虚拟基类 D。
  3. 非虚拟基类 C。
  4. 虚拟基类 B。
  5. 虚拟基类 A。

此过程将生成唯一项的有序列表。任何类名均不会出现两次。在构造列表后,将以相反的顺序遍历该列表,并且将调用列表中每个类(从最后一个到第一个)的析构函数。

如果某个类中的构造函数或析构函数依赖于要先创建或保留更长时间的另一个组件(例如,如果 A 的析构函数(上图中所示)依赖于执行其代码时仍存在 B),则构造或析构的顺序特别重要,反之亦然。
继承关系图中各个类之间的这种相互依赖项本质上是危险的,因为稍后派生类可以更改最左边的路径,从而更改构造和析构的顺序。
非虚拟基类
按照相反的顺序(按此顺序声明基类名称)调用非虚拟基类的析构函数。考虑下列类声明:

class MultInherit : public Base1, public Base2
...

在前面的示例中,先于 Base2 的析构函数调用 Base1 的析构函数。
显式析构函数调用
很少需要显式调用析构函数。但是,对置于绝对地址的对象进行清理会很有用。这些对象通常使用采用位置参数的用户定义的 new 运算符进行分配。 delete 运算符不能释放该内存,因为它不是从自由存储区分配的(有关详细信息,请参阅 new 和 delete 运算符)。但是,对析构函数的调用可以执行相应的清理。若要显式调用 s 类的对象 String 的析构函数,请使用下列语句之一:

s.String::~String();   // Nonvirtual call
ps->String::~String();  // Nonvirtual call

s.~String();    // Virtual call
ps->~String();   // Virtual call

可以使用对前面显示的析构函数的显式调用的表示法,无论类型是否定义了析构函数。这允许您进行此类显式调用,而无需了解是否为此类型定义了析构函数。显式调用析构函数,其中未定义的析构函数无效。

(0)

相关推荐

  • 深入解析C++中的构造函数和析构函数

    构造函数:在类实例化对象时自动执行,对类中的数据进行初始化.构造函数可以从载,可以有多个,但是只能有一个缺省构造函数. 析构函数:在撤销对象占用的内存之前,进行一些操作的函数.析构函数不能被重载,只能有一个. 调用构造函数和析构函数的顺序:先构造的后析构,后构造的先折构.它相当于一个栈,先进后出. 复制代码 代码如下: #include<iostream>#include<string>using namespace std;class Student{ public:  Stud

  • 简要解读C++的动态和静态关联以及虚析构函数

    C++静态关联与动态关联.C++是怎样实现多态性的 在现实生活中,多态性的例子是很多的.我们分析一下人是怎样处理多 态性的.例如,新生被录取人大学,在人学报到时,先有一名工作人员审查材料,他的职责是甄别资格,然后根据录取通知书上注明的录取的系和专业,将材料转到有关的系和专业,办理具体的注册人学手续,也可以看作调用不同部门的处理程序办理入学手续.在学 生眼里,这名工作人员是总的人口,所有新生办入学手续都要经过他.学生拿的是统一的录取通知书,但实际上分属不同的系,要进行不同的注册手续,这就是多态.那

  • 深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结

    1 . 用同一个类的源对象构造一个目标对象时,会调用拷贝构造函数来构造目标对象,如果没有定义拷贝构造函数,将调用类的默认拷贝函数来构造目标对象.2 . 当一个函数的返回值为一个类的对象时,如果在调用函数中,没有定义一个对象来接收这个返回对象值,会用返回一个临时对象保存返回对象的值.在被调用函数结束时,这个临时对象被销毁.而当调用函数中有一个接受对象时,就将返回对象赋值给接收对象,这个返回对象在调用函数结束时调用析构函数.3. 当类有一个带有一个参数的构造函数时,可以用这个参数同类型的数据初始化这

  • 解析C++中虚析构函数的作用

    我们知道,用C++开发的时候,用来做基类的类的析构函数一般都是虚函数.可是,为什么要这样做呢?下面用一个小例子来说明:    有下面的两个类: 复制代码 代码如下: class ClxBase{public:    ClxBase() {};    virtual ~ClxBase() {};    virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };}

  • C++函数返回值为对象时,构造析构函数的执行细节

    看如下代码: 复制代码 代码如下: #include<iostream>class TestConstructor{public:    TestConstructor()    {        std::cout<<"TestConstructor()"<<std::endl;    }    ~TestConstructor()    {        std::cout<<"~TestConstructor()"

  • 详解C++编程中的析构函数

    C++析构函数 创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作(例如回收创建对象时消耗的各种资源),这个函数被称为析构函数. 析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要用户调用,而是在销毁对象时自动执行.与构造函数不同的是,析构函数的名字是在类名前面加一个"~"符号. 注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数.如果用户没有定义,那么编译器会自动生成. 析构函数举例: #inc

  • C++虚析构函数的使用分析

    在C++中,不能声明虚构造函数,但可以声明虚析构函数.多态性是指不同的对象对同一消息有不同的行为特性.虚函数作为运行时多态性的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数是没有意义的.析构函数的功能是在该类对象消亡之前进行一些必要的清理工作,析构函数最好都是virtual的.首先解释一下虚构函数和指针之间是如何交互的,以及虚析构函数的具体含义.例如以下代码,其中SomeClass是含有非virtual析构函数的一个类:SomeClass *p= new SomeCla

  • 浅谈C++基类的析构函数为虚函数

    1.原因: 在实现多态时, 当用基类指针操作派生类, 在析构时候防止只析构基类而不析构派生类. 2.例子: (1). #include<iostream> using namespace std; class Base{ public: Base() {}; ~Base() {cout << "Output from the destructor of class Base!" << endl;}; void DoSomething() { cout

  • 简单介绍C++编程中派生类的析构函数

    和构造函数类似,析构函数也是不能被继承的. 创建派生类对象时,构造函数的调用顺序和继承顺序相同,先执行基类构造函数,然后再执行派生类的构造函数.但是对于析构函数,调用顺序恰好相反,即先执行派生类的析构函数,然后再执行基类的析构函数. 请看下面的例子: #include <iostream> using namespace std; class A{ public: A(){cout<<"A constructor"<<endl;} ~A(){cout

  • c++基础语法:构造函数与析构函数

    说实话c++还是以前在学校的时候用过的,从毕业到现在一直用c嵌入式编程,现在重新搬出C++语法 ,如果理解上有错误的地方,还请路过的朋友多指正--- 构造函数用来构造一个对象,主要完成一些初始化工作,如果类中不提供构造函数,编译器会默认的提供一个默认构造函数(参数为空的构造函数就是默认构造函数) :析构函数是隐式调用的,delete对象时候会自动调用完成对象的清理工作. 现在主要看看继承中的构造函数和析构函数的调用: 复制代码 代码如下: class  A {} :class  B : publ

  • C++类成员构造函数和析构函数顺序示例详细讲解

    对象并不是突然建立起来的,创建对象必须时必须同时创建父类以及包含于其中的对象.C++遵循如下的创建顺序: (1)如果某个类具体基类,执行基类的默认构造函数. (2)类的非静态数据成员,按照声明的顺序创建. (3)执行该类的构造函数. 即构造类时,会先构造其父类,然后创建类成员,最后调用本身的构造函数. 下面看一个例子吧 复制代码 代码如下: class c{public:    c(){ printf("c\n"); }protected:private:}; class b {pub

  • 从汇编看c++的默认析构函数的使用详解

    c++中,如果没有为一个类提供析构函数,那么编译器会为这个类提供默认的析构的函数.由于析构函数的功能和构造函数相反,因此和默认的构造函数类似,编译器也会提供无用的默认的析构函数,和非无用的析构函数.两者的分析情况一样(对于默认的构造函数分析,请参看<从汇编看c++中默认构造函数的使用分析>).并且编译器会提供非无用的默认析构函数情形和默认构造函数类似: 1 类含有虚成员函数(类继承自虚基类或者继承的基类含有虚成员函数,也属于这种情况) 2 类继承自一个基类,基类含有自定义析构函数(如果基类没有

随机推荐