深度解读《深度探索C++对象模型》之C++对象的析构

目录

何时需要析构函数

编译器合成析构函数的情况

对象析构的流程


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。

深入分析C++对象的构造过程系列:

深度解读《深度探索C++对象模型》之C++对象的构造过程(一)

深度解读《深度探索C++对象模型》之C++对象的构造过程(二)

深度解读《深度探索C++对象模型》之C++对象的构造过程(三)

何时需要析构函数

        何时需要一个析构函数?在对象的生命周期结束时必须要调用一个析构函数来销毁它吗?还是说有定义了构造函数的情况下就必须要定义一个析构函数,以实现资源申请和释放的对称性策略?以何为标准来决定是否要提供一个析构函数,我们接下来将来探讨这个问题。

        是否需要提供一个析构函数,实际上要看程序是否需要,而非感觉上是否需要。对象被销毁时是否一定要调用一个析构函数?比如局部对象在生命周期结束时或者new出来的对象在被delete时,这时需要析构函数吗?答案是视情况而定,如果在类的构造函数或者其他的成员函数中没有申请系统资源(如内存、硬件资源等),那么这时候不需要析构函数,对象本身所占用的内存空间会被系统释放掉,不需要析构函数来完成。

        类中有构造函数的时候需要对称的需要一个析构函数吗?这个也不一定,如果构造函数中有申请了系统资源,如申请了内存,那么在对象销毁时则需要归还给系统,否则会导致内存泄漏,这时就需要一个析构函数来完成这个事情。如果提供构造函数的作用只是想要在对象被构造时为它分配一个唯一的ID号,这种情况在对象销毁时并不需要归还资源给系统,所以不需要一个析构函数,除非你想要在对象析构时做好记录,那么才需要实现一个析构函数来完成这个功能。构造函数和析构函数的作用并不完全对称,在大多数情况下,构造函数是必要的,但析构函数却不一定需要。如以下的类定义:

class Object {
public:
	Object(): x(0), y(0) {}
private:
	int x;
	int y;
};

        类中的构造函数是必要的,否则往往程序就会遭遇错误,因为类中数据成员未被正确初始化,直接使用它们将引起不可预期的结果,但是析构函数却不是必须的,如下的析构函数的定义:

~Object() {
    x = 0;
    y = 0;
}

        是不必要的。没有规定说在对象被销毁时要把它的内容清除干净,这时也没有资源需要归还给系统。这时为它提供一个析构函数反而是低效的,因为在每个对象被销毁时都要调用它。

        因此,是否提供需要提供析构函数取决于程序的需求,当程序有需要时就定义它,没有需要则忽略它。但有时程序中没有需要,编译器却需要一个析构函数,所以这时编译器就会合成一个析构函数出来。

编译器合成析构函数的情况

        编译器需要合成出来析构函数的情况一般有两种,一种是类的父类中有定义了析构函数,另一种是类中含有类类型的成员且定义了析构函数。在这两种情况,如果类中没有定义析构函数,编译器就会为它合成一个析构函数,如果类中有定义了析构函数,编译器则会扩充它,目的就是去调用父类的或者类类型成员的析构函数。如以下的例子:

class Base {
public:
    Base(): b(0) {}
    ~Base() {}
private:
    int b;
};

class Object: public Base {
public:
    Object(): d(0) {}
private:
    int d;
};

int main() {
    Object obj;

    return 0;
}

        Base类中有定义了析构函数,Object类中没有定义析构函数,但是编译器为它生成了一个析构函数,来看看编译器生成的汇编代码:

Object::~Object() [base object destructor]:			# @Object::~Object() [base object destructor]
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     qword ptr [rbp - 8], rdi
    mov     rdi, qword ptr [rbp - 8]
    call    Base::~Base() [base object destructor]
    add     rsp, 16
    pop     rbp
    ret
main:								# @main
    # 略...
    call    Object::Object() [base object constructor]
    # 略...
    call    Object::~Object() [base object destructor]
    # 略...

        main函数中调用了Object类的构造函数后就马上调用了它的析构函数,Object类的析构函数特意贴出它的全部代码,这个是编译器生成的代码,它的目的主要是去调用Base类的析构函数,除此之外并无它用,没有看到它会去清除类中的数据成员,因为不需要。类中含有类类型成员的情况和此差不多,就不再一一分析了。

        需要注意的是,当类中有虚函数时或者父类中有一个虚基类时,如果类中没有定义任何构造函数,编译器会为类合成一个默认构造函数,但是这时编译器是不会为类合成析构函数的。

对象析构的流程

        如果类中有析构函数(无论是自定义的还是编译器合成的),当对象的生命结束时,它的析构函数将会被调用,对象析构的流程为:

  1. 如果对象中含有虚函数表指针vptr,它的值会被重设,其实就是重新赋值,所指的还是这个类的虚函数表。
  2. 执行析构函数体内的代码(程序员所写的代码)。
  3. 如果类中含有类类型的成员且有定义了析构函数,那么它的析构函数会被调用,以声明它们的顺序的相反顺序调用之。
  4. 如果有一个直接基类且定义了析构函数,那么基类的析构函数会被调用,如果基类之上还有基类,那么将依次调用它们的析构函数;如果是多重继承的情况,那么将按照声明顺序的相反顺序依次调用基类的析构函数。
  5. 如果有虚基类且有析构函数,如果此类是最派生类(即继承体系的最末端),那么将会以声明顺序的相反顺序调用它们的析构函数。

        我们以一个具体的例子来详细分析上面的流程:

#include <cstdio>

class Grand {
public:
    Grand(int i): g(i) { printf("%s: g = %d\n", __PRETTY_FUNCTION__, g); }
    virtual ~Grand() {
        printf("%s\n", __PRETTY_FUNCTION__);
        print();
    }
    virtual void print() { printf("%s\n", __PRETTY_FUNCTION__); }
private:
    int g;
};

class Base1: virtual public Grand {
public:
    Base1(int a, int b): Grand(a), b1(b) { printf("%s: b1 = %d\n", __PRETTY_FUNCTION__, b1); }
    virtual ~Base1() {
        printf("%s\n", __PRETTY_FUNCTION__);
        print();
    }
    void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
    int b1;
};

class Base2: virtual public Grand {
public:
    Base2(int a, int b): Grand(a), b2(b) { printf("%s: b2 = %d\n", __PRETTY_FUNCTION__, b2); }
    virtual ~Base2() {
        printf("%s\n", __PRETTY_FUNCTION__);
        print();
    }
    void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
    int b2;
};

class Derived: public Base1, public Base2 {
public:
    Derived(): Grand(0), Base1(1, 2), Base2(3, 4), d{0} { printf("%s\n", __PRETTY_FUNCTION__); }
    virtual ~Derived() {
        printf("%s\n", __PRETTY_FUNCTION__);
        print();
    }
    void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
    int d;
};

int main() {
    Derived d;
    return 0;
}

        代码的输出结果:

Grand::Grand(int): g = 0
Base1::Base1(int, int): b1 = 2
Base2::Base2(int, int): b2 = 4
Derived::Derived()
virtual Derived::~Derived()
virtual void Derived::print()
virtual Base2::~Base2()
virtual void Base2::print()
virtual Base1::~Base1()
virtual void Base1::print()
virtual Grand::~Grand()
virtual void Grand::print()

        对比对象的构造过程,对象的析构过程并不完全是构造过程的相反顺序,如构造过程是先构造虚基类、再构造基类,然后设置vptr,初始化类成员(如类类型成员有构造函数会调用之),最后执行构造函数的本体代码。但是对象的析构过程是先设置vptr,这个vptr也是指向本类的虚函数表(跟构造函数设置的一样),这样是保证在析构函数中调用虚函数能正确调用到这个类的虚函数,或者在析构函数中能正确地访问虚基类的成员。如上面代码中在各自析构函数中调用虚函数print,都是调用到对应类的虚函数实例,见上面的输出结果。我们来看看析构函数的汇编代码:

Derived::~Derived() [complete object destructor]:		# @Derived::~Derived() [complete object destructor]
    # 略...
    lea     rsi, [rip + VTT for Derived]
    call    Derived::~Derived() [base object destructor]
    mov     rdi, qword ptr [rbp - 16]       # 8-byte Reload
    add     rdi, 32
    call    Grand::~Grand() [base object destructor]
    # 略...
Derived::~Derived() [base object destructor]:				# @Derived::~Derived() [base object destructor]
    # 略...
    mov     qword ptr [rbp - 8], rdi
    mov     qword ptr [rbp - 16], rsi
    mov     rax, qword ptr [rbp - 8]
    mov     qword ptr [rbp - 32], rax       # 8-byte Spill
    mov     rcx, qword ptr [rbp - 16]
    mov     qword ptr [rbp - 24], rcx       # 8-byte Spill
    mov     rdx, qword ptr [rcx]
    mov     qword ptr [rax], rdx
    mov     rsi, qword ptr [rcx + 40]
    mov     rdx, qword ptr [rax]
    mov     rdx, qword ptr [rdx - 24]
    mov     qword ptr [rax + rdx], rsi
    mov     rcx, qword ptr [rcx + 48]
    mov     qword ptr [rax + 16], rcx
    lea     rdi, [rip + .L.str]
    lea     rsi, [rip + .L__PRETTY_FUNCTION__._ZN7DerivedD2Ev [base object destructor]]
    xor     eax, eax
    call    printf@PLT
    jmp     .LBB32_1
.LBB32_1:
    mov     rdi, qword ptr [rbp - 32]       # 8-byte Reload
    mov     rax, qword ptr [rdi]
    mov     rax, qword ptr [rax + 16]
    call    rax
    jmp     .LBB32_2
.LBB32_2:
    mov     rdi, qword ptr [rbp - 32]       # 8-byte Reload
    mov     rsi, qword ptr [rbp - 24]       # 8-byte Reload
    add     rdi, 16
    add     rsi, 24
    call    Base2::~Base2() [base object destructor]
    mov     rsi, qword ptr [rbp - 24]       # 8-byte Reload
    mov     rdi, qword ptr [rbp - 32]       # 8-byte Reload
    add     rsi, 8
    call    Base1::~Base1() [base object destructor]
    # 略...

        编译器会生成两个版本的析构函数:“complete object destructor”和“base object destructor”,complete版本完成的是全功能的析构函数,是当一个完整的对象析构时被调用,base版本是当子对象析构时被调用的。Base1类和Base2类同样也是有两个版本,因为Base1Base2作为Derived对象的子对象,所以这里调用的是它们的base版本(见上面的代码第41和45行)。complete版本会去析构虚基类,在上面的代码里,complete版本的析构函数先调用base版本的析构函数完成基本的工作,然后调用虚基类Grand的析构函数,在base版本的析构函数里调用了Base1类和Base2类的base版本的析构函数,在这两个析构函数里则不会调用虚基类Grand的析构函数。

        上面代码的第11到24行就是设置虚函数表指针vptr的值,第11行的rdi寄存器保存的是对象d的首地址,第12行的rsi寄存器保存的是虚表的首地址(见第3行代码),这里先将它们保存到栈空间[rbp - 8]和[rbp - 16]中。这里会设置三个vptr值,分别是Derived对象/Base1子对象(这两者共用一个)(第15到18行)、Grand虚基类子对象(第19到22行)和Base2子对象(第23、24行)。

        接着是执行析构函数体内的代码,首先调用printf函数(第25到28行),接着是代码的第31到34行是调用虚函数print,这里使用的是虚拟调用的机制,虚函数表指针在之前的代码中已设置完毕。

        然后是代码中的第37到45行是以声明顺序的相反顺序调用Base2Base1的析构函数。之后base版本的析构函数结束并返回到complete版本的析构函数,最后调用虚基类Grand的析构函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/584434.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第三节课,功能2:开发后端用户的管理接口5min(用户的查询/状态更改)【4】【9开始--本人】

一、代码任务 【录个屏】 二、写代码 2.1 代码文件位置 2.2 代码如下&#xff1a; 2.3 官方文档&#xff1a; 网址&#xff1a; 逻辑删除 | MyBatis-Plus (baomidou.com) 三、代码有bug&#xff0c;没有鉴权&#xff0c;表里添加一个字段。role 管理员 3.1 判断操作的人&am…

【开发工具】pythontutor——在线内存可视化工具

笔者在学习RISC-V时&#xff0c;希望找到一款可视化的内存工具&#xff0c;遗憾目前还未找到。发现了pythontutor这个网站&#xff0c;可以对C、python等多种语言进行内存可视化。结果似乎是x86架构的&#xff0c;符合小端存储。 贴一下网址&#xff0c;原准备依据开源版本进行…

什么是MOM?为什么它是趋势

制造运营管理&#xff08;MOM&#xff09; 制造运营管理&#xff08;MOM&#xff09;旨在优化制造流程的效率和有效性。它涵盖制造执行、质量管理、生产计划和调度以及制造智能等功能。这种解决方案以全面的方式管理和增强制造流程。 MOM的功能特点 对于MOM的功能特点来说&…

爬虫自动调用shell通过脚本运行scrapy爬虫(crawler API)

一、爬虫时如何同时调用shell 1)终端cd项目>>scrapy crawl example 2)打开example.py import scrapy from scrapy.shell import inspect_response#引入shellclass ExampleSpider(scrapy.Spider):name "example"allowed_domains ["example.com"]…

兄弟们,哪一家做智慧校园的公司比较靠谱?

开发技术参数 1、使用springboot框架Javavue2 2、数据库MySQL5.7 3、移动端小程序使用小程序原生语言开发 4、电子班牌固件安卓7.1&#xff1b;使用Java Android原生 5、elmentui &#xff0c;Quartz&#xff0c;jpa&#xff0c;jwt 6、SaaS云平台&#xff0c;私有云部署…

iview 自定义项求和的方法和错误点

这是iview自定义某几项参数合计的方法&#xff0c;其实是蛮简单的&#xff0c;很多人自定义合计的时候&#xff0c;老是会不知道怎么处理除了需要合计的几项的其他项&#xff0c;其实不需要管&#xff0c;不需要合计的项直接返回空就好了&#xff0c;需要的就在计算的里面做key…

Llama 3 ——开源大模型Llama 3从概念到使用

概述 Meta公司自豪地宣布推出其最新的开源大型语言模型——Llama 3&#xff0c;这是一款专为未来AI挑战而设计的先进工具。Llama 3包含两个不同参数规模的版本&#xff0c;以满足多样化的计算需求&#xff1a; 8B版本&#xff1a;优化了在消费级GPU上的部署和开发流程&#xf…

初识MVC

初识MVC 理论部分 今天第一次学MVC&#xff0c;拿到一个练手项目。现在来记录一下学习过程。 项目的背景就是个学生管理系统。我只做后端。 从大的来说MVC将应用程序分为三个主要组件&#xff08;部分&#xff09;&#xff1a; 模型&#xff08;Model&#xff09;是应用程序…

SGP.31-05

6.1.1 eIM触发下载过程 6.1.2 eIM Initiated Direct Profile Download with SM-DS 在文档的“6.1.2 eIM 发起的通过 SM-DS 的直接配置文件下载”部分&#xff0c;描述了两种直接从 SM-DP 到 eUICC 的配置文件下载选项。以下是每个步骤的概述&#xff1a; ### 开始条件&…

​基于Python的在线自主评测系统(django)​

基于Python的在线自主评测系统(django) 开发语言:Python 数据库&#xff1a;MySQL所用到的知识&#xff1a;Django框架工具&#xff1a;pycharm、Navicat、Maven 学生功能模块的实现 学生注册的实现 学生登录界面首页 在线考试界面 考试成绩查看界面 教师功能模块的实现 新建…

以全栈智算拥抱生态,为AIGC种一棵向上生长的巨榕

榕树&#xff0c;被称为百木之王。它既有极深的根&#xff0c;又有繁茂的叶。只要一棵榕树长成&#xff0c;就能够独木成林&#xff0c;遮天蔽日。更可贵的是&#xff0c;榕树可以为树荫下繁茂的生态提供支撑&#xff0c;形成“一榕生&#xff0c;万物长”的格局。 开年以来&am…

MySQL中SELECT语句的执行过程

2.1.1. 一条SELECT语句的执行过程 MySQL 的架构共分为两层&#xff1a;Server 层和存储引擎层 Server层负责建立连接、分析和执行SQL存储引擎层负责数据的存储和提取&#xff0c;支持 InnoDB、MyISAM、Memory 等多个存储引擎&#xff0c;MySQL5.5以后默认使用InnoDB&#xff0…

set_input_delay的理解

1&#xff0c;set_input_delay约束理解 input_delay是指输入的数据到达FPGA的pad引脚时相对于时钟边沿的延迟有多大&#xff0c;单位是ns&#xff0c;数值可以是正&#xff0c;也可以是负。通过set_input_delay约束告诉编译器输入时钟和输入数据的相位关系。如下图所示假设时钟…

RSA加密---java和node兼容版(可直接复制使用)

目录 背景 实现 一、node代码 1、引入依赖 2、生成公钥和私钥 3、生成工具类 二、java代码 背景 本来项目的后端是node&#xff0c;里面登录接口用的是后端生成RSA公钥和私钥&#xff0c;公钥给前端网页用来加密&#xff0c;node后端解密&#xff0c;一切很和谐&#x…

Flexible布局在Web前端开发中的实际应用

随着Web前端技术的不断发展&#xff0c;Flexible布局&#xff08;弹性布局&#xff09;已成为现代网页设计中不可或缺的一部分。它提供了一种高效、灵活的方式来组织和管理页面元素&#xff0c;使开发者能够轻松应对各种复杂的布局需求。本文将通过一个实际的应用案例来介绍Fle…

鸿蒙内核源码分析(汇编基础篇) | CPU在哪里打卡上班

本篇通过拆解一段很简单的汇编代码来快速认识汇编&#xff0c;为读懂鸿蒙汇编打基础.系列篇后续将逐个剖析鸿蒙的汇编文件. 汇编很简单 第一&#xff1a; 要认定汇编语言一定是简单的&#xff0c;没有高深的东西&#xff0c;无非就是数据的搬来搬去&#xff0c;运行时数据主要…

阿里云服务器(Ubuntu22)上的MySQL8更改为大小写不敏感

因为windows上默认的mysql8.0是大小写不敏感的&#xff0c;部署到服务器上之后发现ubuntu默认的是大小写敏感&#xff0c;所以为了不更改代码&#xff0c;需要将mysql数据库设置为大小写不敏感的。 &#xff01;&#xff01;&#xff01;重要一定要做好数据库的备份&#xff0…

【Vue3】openlayers加载瓦片地图并手动标记坐标点

目录 一、创建Vue3项目 二、openlayers加载瓦片地图&#xff08;引js文件版&#xff09; 2.1 将以下的文件复制到public下 2.2 index.html引入ol脚本 2.3 删除项目自带的HelloWorld.vue&#xff0c;创建Map.vue 2.4 编码Map.vue 2.5 修改App.vue 2.6 启动项目测试 三、…

与Apollo共创生态:Apollo 7周年大会带给我的启发和心得

Apollo 7周年大会 前不久的Apollo 7周年大会&#xff0c;吸引到我这个对自动驾驶有着浓厚兴趣的开发者&#xff0c;真的精彩&#xff0c;受益匪浅。Apollo 7周年大会展示了Apollo在自动驾驶领域的创新成果&#xff0c;探讨自动驾驶技术的未来发展趋势&#xff0c;并推动自动驾…

关键技术自主可控,中国移动发布大云磐石DPU芯片,速率达400Gbps

4月28日&#xff0c;中国移动在2024算力网络大会上正式发布大云磐石DPU&#xff0c;该芯片带宽达到400Gbps&#xff0c;为国内领先水平&#xff0c;将应用于移动云新一代大云磐石DPU产品&#xff0c;实现关键技术自主可控。 据介绍&#xff0c;DPU是一种专注于数据处理的处理器…
最新文章