最近发现自己对析构函数的认知有一定的问题,因为之前有在使用placement new时主动调用对象的析构函数,所以觉得析构函数只是个普通的成员函数,调用的时候只会执行自己方法体内的代码内容,而回收内存则是由于生命周期到了操作系统自动回收的。不过昨天突然在想,那如果是派生类的话,主动调用析构函数的话,基类的内容怎么回收呢。于是,发现了自己长期以来的基础错误,也说明了自己还是需要不断巩固基础啊。话不多说,直接开始介绍。

“析构函数”是构造函数的反向函数。 在销毁(释放)对象时将调用它们。 通过在类名前面放置一个波形符 (~) 将函数指定为类的析构函数。 例如,声明 String 类的析构函数:~String()

在 /clr 编译中,析构函数在释放托管和非托管资源方面发挥了特殊作用。 有关详细信息,请参阅Visual C++ 中的析构函数和终结器

析构函数通常用于在不再需要某个对象时“清理”此对象。 请考虑 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 ) + ; // 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 语句返回值。

  • 无法声明为 constvolatile 或 static。 但是,可以为声明为 constvolatile 或 static 的对象的析构调用它们。

  • 可以声明为 virtual。 通过使用虚拟析构函数,无需知道对象的类型即可销毁对象 - 使用虚函数机制调用该对象的正确析构函数。 请注意,析构函数也可以声明为抽象类的纯虚函数。

当下列事件之一发生时,将调用析构函数:

  • 使用 delete 运算符显式解除分配了使用 new 运算符分配的对象。 使用 delete 运算符解除分配对象时,将为“大多数派生对象”或为属于完整对象,但不是表示基类的子对象的对象释放内存。 此“大多数派生对象”解除分配一定仅对虚拟析构函数有效。 在类型信息与实际对象的基础类型不匹配的多重继承情况下,取消分配可能失败。

  • 具有块范围的本地(自动)对象超出范围。

  • 临时对象的生存期结束。

  • 程序结束,并且存在全局或静态对象。

  • 使用析构函数的完全限定名显式调用了析构函数。 (有关详细信息,请参阅显式析构函数调用。)

前面的列表中所述的情况将确保所有对象均可通过用户定义的方法进行销毁。

如果基类或数据成员有一个可访问的析构函数,并且派生类未声明析构函数,则编译器将生成一个析构函数。 此编译器生成的析构函数将为派生类型的成员调用基类析构函数和析构函数。 默认析构函数是公共的。 (有关可访问性的详细信息,请参阅基类的访问修饰符。)

析构函数可以随意调用类成员函数和访问类成员数据。 从析构函数调用虚函数时,调用的函数是当前正在销毁的类的函数。 (有关详细信息,请参阅析构函数的顺序。)

析构函数的使用有两个限制。 第一个限制是您无法采用析构函数的地址。 第二个是派生类不会继承其基类的析构函数。 相反,如前所释,它们始终重写基类的析构函数。

当对象超出范围或被删除时,其完整析构中的事件序列如下所示:

  1. 将调用该类的析构函数,并且会执行该析构函数的主体。

  2. 按照非静态成员对象的析构函数在类声明中的显示顺序的相反顺序调用这些函数。 用于这些成员的构造的可选成员优化列表不影响构造或析构的顺序。 (有关初始化成员的详细信息,请参阅初始化基和成员。)

  3. 非虚拟基类的析构函数以声明的相反顺序被调用。

  4. 虚拟基类的析构函数以声明的相反顺序被调用。

 // 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; //或者b2->~B3(),结果相同
} Output: 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

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

析构函数 (C++)的更多相关文章

  1. .NET 基础 一步步 一幕幕[面向对象之构造函数、析构函数]

    构造函数.析构函数 构造函数: 语法: //无参的构造函数 [访问修饰符] 函数名() :函数名必须与类名相同. //有参的构造函数 [访问修饰符] 函数名(参数列表):函数名必须与类名相同. 作用: ...

  2. 【C#】析构函数

    MSDN paper 析构函数 析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数. 析构函数往往用来做“清理善后” 的工作( ...

  3. c++虚析构函数

    虚析构函数的作用主要是当通过基类指针删除派生类对象时,调用派生类的析构函数(如果没有将不会调用派生类析构函数) #include <iostream> using namespace st ...

  4. 转:Delphi2010新发现-类的构造和析构函数功能

    Delphi2010发布了. 虽然凭着对Delphi的热爱第一时间就安装了,但是现在可能是年纪大了,对新事物缺乏兴趣了.一直都没有仔细研究. 今天有点时间试了一下新功能. 本来C#和Delphi.NE ...

  5. c++单例模式为什么不在析构函数中释放静态的单例对象(转)

    需要清楚一下几点:   1.单例中的 new 的对象需要delete释放.   2.delete释放对象的时候才会调用对象的析构函数.   3.如果在析构函数里调用delete,那么程序结束时,根本进 ...

  6. C++构造函数/析构函数 设置成private的原因

    C++构造函数/析构函数 设置成private的原因 标签(空格分隔): c/c++ 将构造函数,析构函数声明为私有和保护的,那么对象如何创建? 已经不能从外部调用构造函数了,但是对象必须被构造,应该 ...

  7. 关于GC和析构函数的一个趣题

    这个有趣的问题感谢装配脑袋友情提供. 请看如下代码: public class Dummy { public static Dummy Instance; ; ~Dummy() { Instance ...

  8. C++构造函数和析构函数

    构造函数简介 在上一个章节我们在创建好类的对象之后,首先对它的每一个成员属性赋值之后再对它们进行输出操作,如果不赋值就输出,这些值就会是垃圾值.而为了代码的简介,一次性为所有成员属性初始化,C++的类 ...

  9. virtual 修饰符与继承对析构函数的影响(C++)

    以前,知道了虚函数表的低效性之后,一直尽量避免使用之.所以,在最近的工程中,所有的析构函数都不是虚函数.今天趁着还书的机会到图书馆,还书之后在 TP 分类下闲逛,偶然读到一本游戏编程书,里面说建议将存 ...

  10. 谈一谈.net析构函数对垃圾回收的影响

    之前忘了说了 代码都是在Release模式下运行的,现在补充上. 这里说析构函数,其实并不准确,应该叫Finalize函数,Finalize函数形式上和c++的析构函数很像 ,都是(~ClassNam ...

随机推荐

  1. j.u.c系列(02)---线程池ThreadPoolExecutor---tomcat实现策略

    写在前面 本文是以同tomcat 7.0.57. jdk版本1.7.0_80为例. 线程池在tomcat中的创建实现为: public abstract class AbstractEndpoint& ...

  2. git 用户名和密码保存

    git config --global credential.helper store 输入一次后,后续不再需要输入用户名密码

  3. JavaScript基础之运算符及全面的运算符优先级总结

    算数运算符: 加+,减—,乘*,除/,求余%,加加++,减减——, 加减乘除求余运算与数学上的用法完全一样. 不过,加号+还有连接字符串的作用,其他运算符还可以将字符串数字转换成数值型,参见JavaS ...

  4. CentOS下KVM网卡设置成网桥时获取镜像端口的流量

    首先,网桥配置好之后就能实现一个简单的交换机,而交换机的特点就是MAC地址学习,那么KVM的网卡设置成网桥之后,也就是相当于连接到了交换机上. 此时如果要实现在二层交换机或三层交换机做端口镜像,并把这 ...

  5. 玩转ptrace(转)

    下面是转帖的内容,写的很详细.但是不同的linux发行版中头文件的路径和名称并不相同.如在某些发行版中<linux/user.h>就不存在,其中定义的变量出现在<asm/ptrace ...

  6. AVR Programming Methods

    AVR Programming Methods  There are many ways to program AVR microcontrollers. Since many people ask ...

  7. 【Go命令教程】13. go tool cgo

    cgo 也是一个 Go 语言自带的特殊工具.一般情况下,我们使用命令 go tool cgo 来运行它.这个工具可以使我们创建能够调用 C 语言代码的 Go 语言源码文件.这使得我们可以使用 Go 语 ...

  8. Revit Family API 添加对齐

    没测试成功,留待以后研究. [TransactionAttribute(Autodesk.Revit.Attributes.TransactionMode.Manual)] ; ; i < nV ...

  9. 阅读Linux内核源码时建立tags索引

    比如在阅读arm架构的Linux内核代码时想建立arm架构相关的索引,可以使用下面的命令: make ARCH=arm tags

  10. 在AngularJS中使用ES6

    本篇记录一些AngularJS结合使用ES6的各种写法. ES6中module的导出导入 class MainController { constructor(searchService){ this ...