深度解读《深度探索C++对象模型》之虚继承的实现分析和效率评测(一)

目录

前言 

具有虚基类的对象的构造过程

通过子类的对象存取虚基类成员的实现分析


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

前言 

        前面几篇分析了静态数据成员、普通的数据成员以及在继承体系下的数据成员的存取效率的分析,请从这里阅读:

        深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)

        深度解读《深度探索C++对象模型》之数据成员的存取效率分析(二)

        深度解读《深度探索C++对象模型》之数据成员的存取效率分析(三)

        接下来来分析虚继承的实现以及它的效率评测,在读这篇文章之前,为了能够更好地理解内容,建议先阅读一下以下的文章,补充一些基础知识。

        深度解读《深度探索C++对象模型》之默认构造函数

        深度解读《深度探索C++对象模型》之C++对象的内存布局(一)

        深度解读《深度探索C++对象模型》之C++对象的内存布局(二)

        深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)

        深度解读《深度探索C++对象模型》之C++虚函数实现分析(二)

        深度解读《深度探索C++对象模型》之C++虚函数实现分析(三)

        深度解读《深度探索C++对象模型》之C++虚函数实现分析(四)

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

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

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

        现在来分析在虚继承时访问虚基类的数据成员的实现方法,以及它和访问普通的数据成员之间的效率对比评测。虚继承虽然很少使用,但可能难以避免有时业务中确实需要用到,这时熟悉编译器对于虚继承的实现手法和存取虚基类成员的效率,这样可以对所写的代码了然于胸,做到心中有数。我们以一个具体的例子来分析:

class Grand {
public:
    virtual ~Grand() {}
    int g;
};
class Base1: virtual public Grand {
public:
	int b1;
};
class Base2: virtual public Grand {
public:
	int b2;
};
class Derived: public Base1, public Base2 {
public:
	int d;
};

int main() {
    Derived d;
    d.g = 5;
    Derived* pd = &d;
    pd->g = 6;
    Base1* pb1 = &d;
    pb1->g = 7;
    Base2* pb2 = &d;
    pb2->g = 8;
    Grand* pg = &d;
    pg->g = 9;
    
    return 0;
}

        要深入分析编译器对虚继承的实现手法,最好的方法是分析编译器生成的汇编代码,上面短短的C++代码生成的汇编代码却相当多,不可能全部贴出来,只能将有需要讲到的地方贴出来。

具有虚基类的对象的构造过程

        首先,main函数的第一行定义了一个Derived类的对象,这里则会去调用Derived类的默认构造函数,在Derived类的构造函数里首先会去调用Grand子类的默认构造函数,然后调用Base1子类和Base2子类的默认构造函数,最后是完成自身的构造。不要奇怪为什么会去调用这些默认构造函数,明明代码中并没有定义这些函数啊,如果对这个有疑问的话可以先看一下另外一篇“深度解读《深度探索C++对象模型》之默认构造函数”。在这些默认构造函数里主要的事情就是去设置虚表指针,因为代码中有虚继承,所以编译器会生成一个虚表,而且虚基类中有定义了虚函数,所以它的派生类中都会继承虚函数(这里指的都是虚析构函数),所以也有一个虚函数表,这些具体的细节不同的编译器有不同的实现手法,clang和gcc是将这两个表合二为一,只需要一个指针指向它们,而MSVC是分开两个表,所以需要两个指针来指向它们,但是原理都大致相同,这里就以clang的实现为例。

        下面是Derived类的构造函数的汇编代码:

        上面汇编代码的前三行是保存上个函数的栈寄存器,然后开辟了16字节的栈空间来使用。接着是将rdi寄存器的值保存到栈空间中,rdi是调用Derived类构造函数时传递过来的参数,它是Derived类的对象d的地址。

        上面汇编代码的第66行,在此地址之上偏移32个字节(跳过Base1子对象和Base2子对象),即为Grand类子对象的起始地址(对对象的内存布局还不熟悉的,可以先参考:

深度解读《深度探索C++对象模型》之C++对象的内存布局(一)

深度解读《深度探索C++对象模型》之C++对象的内存布局(二)

),这时将rdi寄存器(对象d的地址偏移了32字节后)作为参数,调用Grand类的默认构造函数。下面是Grand类的默认构造函数汇编代码:

        汇编代码的第110行到112行,在Grand类的默认构造函数里会先设置Grand类的虚函数表指针(指向Grand类的虚函数表,每个类都会有一个虚函数表),[rip + vtable for Grand]是虚表的地址,以下这个表的内容:

        前面两行先不管它,第三、四行即存放虚函数的地址。所以上面汇编代码的第111行里跳过16字节,即是跳过了前面两行,将第三行的内容即虚函数的地址,设置给Grand类子对象的起始地址中,至此完成了Grand类子对象的虚函数表的设置。

        回到Derived类的构造函数的汇编代码中,见第68行到71行,这里是去调用Base1子类的默认构造函数:

        [rbp - 16] 栈空间保存的是Derived类的对象d的地址,这里再加载到rdi寄存器中,作为调用Base1类默认构造函数的第一个参数。第69行代码是取得“VTT for Derived”表的地址并保存在rsi寄存器中,作为调用Base1类默认构造函数的第二个参数。

        “VTT for Derived”表的内容如下:

        上面汇编代码的第70行将rsi里的值加上8的偏移值,也就是上表的起始地址加上8,实际上就是指向第二条内容的地址,最后第71行代码调用Base1类的默认构造函数。

        Base1类的默认构造函数代码:

        第118行、119行代码将第一个参数rdi寄存器和第二个参数rsi寄存器中的内容分别保存到栈空间[rbp - 8]和[rbp - 16]中。从上面的分析中我们知道,rsi保存的内容是“construction vtable for Base1-in-Derived+24”,它实际上是表“construction vtable for Base1-in-Derived”的起始地址加上偏移值24的意思。那么来看下“construction vtable for Base1-in-Derived”表的内容:

        上面的表加上24的偏移值,实际上就是跳过前面三行的内容,指向第四条的内容,也就是Base1类的虚析构函数的地址。然后上面汇编代码的第122行、123行将这个地址设置给Base1子对象的起始位置,这个就是之前说过的设置虚函数表指针。

        接下来的第124行到127行的代码意思跟前面的差不多,只不过它设置的虚基类子对象的虚函数表指针。第124行的rcx + 8,rcx原先的内容是“VTT for Derived”表的第二行即“construction vtable for Base1-in-Derived+24”,这里再加8就是指向第三行,并将它的内容保存到rdx寄存器中。第126行的rcx - 24,实际上就是跳回到“construction vtable for Base1-in-Derived”表的起始位置,然后对其取值,也就是32(参见上面的表)并保存到rcx寄存器中。在汇编代码的第127行,rax + rcx表示对象d的起始地址(也是Base1子对象的起始地址)加上32的偏移值,定位到虚基类Grand类的子对象的起始地址,并将虚函数表指针设置到这个起始地址中。

        接下来的Base2子对象的构造过程跟构造Base1子对象的过程类似,不同的是设置的虚函数表指针的内容不同。最后是Derived类子对象的构造,过程都大同小异,这里就不再赘述。

        通过上面的分析我们知道,在构造Base1和Base2子类的时候,除了设置Base1和Base2自身的虚函数表指针之外,还会重新设置Grand类的虚函数表指针(设置两次,一次设置为指向Base1类的,后一次设置为指向Base2类的),最后在构造Derived类的时候全都更新为指向Derived类的虚函数表。

        构造完Derived类的对象后,接着来分析存取虚基类的数据成员g,我们采取几种不同的途径来存取,如通过Derived类的对象、Derived类型的指针、Base1和Base2父类的指针以及虚基类Grand类型的指针来存取数据成员g,分别分析它们的实现手法有什么区别。

通过子类的对象存取虚基类成员的实现分析

        首先通过对象来存取,C++代码第21行:d.g = 5;,对应的汇编代码如下:

mov     rax, qword ptr [rbp - 56]
mov     rax, qword ptr [rax - 24]
mov     dword ptr [rbp + rax - 48], 5

        [rbp - 56]是对象Derived对象d的地址,这个地址在构造对象d的最后阶段的时候被写入虚函数表指针:

mov     rax, qword ptr [rbp - 16]       # 8-byte Reload
lea     rcx, [rip + vtable for Derived]
add     rcx, 24
mov     qword ptr [rax], rcx

        第2行是加载虚表的地址到rcx寄存器(这个虚表包含了虚基类表和虚函数表),然后加上偏移值24写入到对象的起始地址中,加上偏移值24后指向了虚函数的地址,下面是Derived类的虚表的内容:

vtable for Derived:
    .quad   32
    .quad   0
    .quad   typeinfo for Derived
    .quad   Derived::~Derived() [complete object destructor]
    .quad   Derived::~Derived() [deleting destructor]
    .quad   16
    .quad   -16
    .quad   typeinfo for Derived
    .quad   non-virtual thunk to Derived::~Derived() [complete object destructor]
    .quad   non-virtual thunk to Derived::~Derived() [deleting destructor]
    .quad   -32
    .quad   -32
    .quad   typeinfo for Derived
    .quad   virtual thunk to Derived::~Derived() [complete object destructor]
    .quad   virtual thunk to Derived::~Derived() [deleting destructor]

        这个表中有几种类型的虚函数,这个主要是跟多态的调用有关,主要是为了实现虚函数的多态调用,这里先不分析,后面再专门讲这个。接着上面的汇编代码,对象d的起始地址的内容现在就是虚表的地址偏移24字节,rax - 24就相当于又指向了虚表的起始地址,[rax - 24]是取这个地址的内容(相当于指针的解引用),也就是32。rbp + rax - 48相当于rbp - 56 + 8 + rax,rbp - 56是对象的起始地址,加上rax即32,是跳过了Base1和Base2两个子类的大小,再加8是因为Grand子类的前面有一个虚函数表指针,大小为8字节,所以最终指向的地址为数据成员g的地址,然后对其赋值为5。

(未完待续。。。敬请点击左下角的关注以获得及时更新)


本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。

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

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

相关文章

docker端口映射成功,docker端口不生效的问题解决,外界无法访问docker映射端口

docker端口映射不生效的问题解决 问题 使用docker run -p 88848:8848后,显示容器启动正常,并且使用docker logs –f xxx能够看到容器可以正常启用,docker ps 可以看到容器启动成功,并且端口已经映射,但是在浏览器访问相关地址&am…

字符串函数(一):strcpy(拷贝),strcat(追加),strcmp(比较),及strncpy,strncat,strncmp

字符串函数 一.strcpy(字符串拷贝)1.函数使用2.模拟实现 二.strcat(字符串追加)1.函数使用2.模拟实现 三.strcmp(字符串比较)1.函数使用2.模拟实现 四.strncpy1.函数使用2.模拟实现 五.strncat1.函数使用2.…

调剂”小清华“、不保护一志愿?——兰州大学25计算机考研考情分析

兰州大学(Lanzhou University),简称“兰大”,是中华人民共和国教育部直属 全国重点大学,中央直管副部级建制,位列国家首批“双一流(A 类)”、“211 工 程”、“985 工程”大学行列,入选国家“珠…

电机及FOC算法介绍

一.电机概述 1.电机的简介 电机是一种可以在电能和机械能的之间相互转换的设备,其中发电机是将机械能转换为电能,电动机是将电能转换为机械能。发电机的主要用于产生电能,用途单一,但是电动机主要用于产生机械能,用途…

外卖 点金推广实战课程,2024外卖 点金推广全流程(7节课+资料)

课程内容: 外卖点金推广实操课程 资料 01 1-了解外卖.mp4 02 第一节:点金推广的说明.mp4 03 第二节:如何降低点金推广的成本,mp4 04 第三节:如何计算点金推广的流速,mp4 05 第四节:如何提升点金的精准度,mp4 06 第五节:点金推广实操,mp4 07 点金推广高级教程…

几种IO模型

部分图来自网络和黑马程序员 IO IO分为两个阶段:数据准备(数据读取到内核缓冲区)数据拷贝(从内核缓冲区拷贝到用户空间) 例如,在下图中两个主机的通信中,程序A/B从TCP接收缓冲区读取数据时&am…

Vue3实战笔记(13)—pinia安装笔记

文章目录 前言安装和配置pinia总结 前言 Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。 Pinia是一个轻量级的状态管理库,它专注于提供一个简单的API来管理应用程序的状态。相比之下,Vuex是一个更完整的状态管理库&#xf…

视频模糊变清晰,这13个工具总有一个能帮到你,收藏好

1、Topaz Video Enhance AI 这是一款非常专业的视频分辨率放大软件,使用来自多个帧的信息来实现视频升级、去噪、去隔行扫描和恢复的结果。 Topaz Video Enhance AI可以将视频放大和增强8K分辨率的镜头,并提供真实的细节和动作一致性。它采用AI技术实现…

【STM32HAL库】DAC输出0-3.3v

一、简要介绍一下DAC DAC也有分辨率,转换时间,精度等 分辨率常见为8或12位的 转换时间F1,F4,F7都是3us左右,而H7系列是1.7us 1.DAC框图 2.数据格式(对齐方式) 3.触发源 4.可以发送DMA请求 注意&#xff…

OSS证书自动续签,一分钟轻松搞定,解决阿里云SSL免费证书每3个月失效问题

文章目录 一、🔥httpsok-v1.11.0支持OSS证书自动部署介绍支持特点 二、废话不多说上教程:1、场景2、实战Stage 1:ssh登录阿里云 ECSStage 2:进入nginx (docker)容器Stage 3:执行如下指令Stage 3…

vivado Virtex UltraScale 配置存储器器件

Virtex UltraScale 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 Virtex UltraScale ™ 器件执行擦除、空白检查、编程和验证等配置操作。 本附录中的表格所列赛灵思系列非易失性存储器将不断保持更新 , 并支持通过 Vivado 软件对其中所列非易失…

Flink HA模式下JobManager切换时发送告警

资源&版本信息 Flink版本1.14.6 运行平台:K8s HA使用ZK(使用K8s的ETC应该是一个道理) 详解Flink HA原理 Flink启动时会创建HighAvailabilityServices提供HA和相关基础服务,其中包括leaderRetrievalService和LeaderElecti…

MP4视频转gif怎么做?看看这篇就会了

喜欢刷短视频的小伙伴经常会看到各种好玩有趣的片段,想要通过自己将这段视频制作成gif动态图片的还不想下载软件的时候要怎么办呢?这个很简单,不需要下载什么软件用专业的Gif动画制作网站,支持超清的画质导出,能够完成…

ssm123基于java web的网上书城系统的设计与实现+vue

基于java web的网上书城系统的设计与实现vue 摘 要 随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势,商品交易当然也不能排除在外,随着商品交易管理的不断成熟,它彻底改变了…

Git详解之六:Git工具

现在,你已经学习了管理或者维护 Git 仓库,实现代码控制所需的大多数日常命令和工作流程。你已经完成了跟踪和提交文件的基本任务,并且发挥了暂存区和轻量级的特性分支及合并的威力。 接下来你将领略到一些 Git 可以实现的非常强大的功能&…

Java抽象类:为何它是你代码架构的基石?

目录 1、抽象类的概念 2、抽象类语法 3、抽象类特性 4、抽象类的作用 5、 完结散花 个人主页:秋风起,再归来~ 文章专栏:javaSE的修炼之路 个人格言:悟已往之不谏,知来者犹可追 克…

WWW服务器搭建(2)——Apache服务器配置与管理

一、Apache简介 1.1 关于Apache Apache HTTP Server(简称Apache)是Apache软件基金会的一个开放源码的Web服务器,可以在大多数计算机操作系统中运行,由于其跨平台和安全性被广泛使用,是最流行的Web服务器端软件之一。…

【Linux 系统】多线程(生产者消费者模型、线程池、STL+智能指针与线程安全、读者写者问题)-- 详解

一、生产者消费者模型(重点) 如图,在生活中,学生就是消费者角色,工厂是真正的生产者角色,那么超市是什么呢?为什么需要超市?超市是交易场所。我们的家附近不一定有工厂,而…

JumpServer发布web应用

项目背景: 由于防火墙密码安全没有达到审计要求,需要加固防火墙用户安全,通过JumpServer发布防火墙登录页面,提供远程访问 认证要求: 1、密码记忆多次 2、密码大小写 3、密码字符 4、密码数字 加固前密码策略&…

【C++要哮着学】初识C++,什么是C++?什么是命名空间?什么又是缺省函数?

文章目录 前言1、C简介1.1、什么是C1.2、C起源1.3、C发展 2、C关键字(C98)3、命名空间3.1、命名空间的定义及使用3.2、命名空间的嵌套3.3、命名空间的三种使用方式3.3.1、加命名空间名称及作用域限定符3.3.2、使用using将命名空间中某个成员引入3.3.3、使…