深度解读《深度探索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爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。
相关文章:

深度解读《深度探索C++对象模型》之虚继承的实现分析和效率评测(一)
目录 前言 具有虚基类的对象的构造过程 通过子类的对象存取虚基类成员的实现分析 接下来我将持续更新“深度解读《深度探索C对象模型》”系列,敬请期待,欢迎左下角点击关注!也可以关注公众号:iShare爱分享,或文章末…...

计算机Java项目|Springboot房产销售系统
作者主页:编程指南针 作者简介:Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容:Java项目、Python项目、前端项目、人工智能与大数据、简…...

学习3D几何和特征一致的高斯溅射目标去除
earning 3D Geometry and Feature Consistent Gaussian Splatting for Object Removal 学习3D几何和特征一致的高斯溅射目标去除 Yuxin Wang 王玉欣 HKUST &Qianyi Wu Monash University &Guofeng Zhang Zhejiang University &Dan Xu HKUST 香港科技大学&吴倩…...

PHP 使用常量实现枚举类
PHP 使用常量实现枚举类 <?php abstract class Enum {private static $constCacheArray NULL;private static function getConstants() {if (self::$constCacheArray NULL) {self::$constCacheArray [];}$calledClass get_called_class();if (!array_key_exists($call…...

Linux操作系统基础题库
一. 单选题(共2题,40分) 1. (单选题)Linux操作系统自诞生至今,有数十万的程序开发人员参与到了它的开发与完善中,如今Linux已发展成为是一个成熟、稳定的操作系统。从以下选项中选出关于Linux特点描述完全正确的一项。…...

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

Flutter 中的 ToggleButtons 小部件:全面指南
Flutter 中的 ToggleButtons 小部件:全面指南 在 Flutter 中,ToggleButtons 是一种允许用户在一组选项中进行切换选择的控件。它通常用于展示一组相关选项,让用户可以快速切换选择。ToggleButtons 是一种水平排列的按钮集合,其中…...

【MYSQL】一颗B+树可以保存多少条数据
引言 事万物都有自己的单元体系,若干个小单体组成一个个大的个体。就像拼乐高一样,可以自由组合。所以说,如果能熟悉最小单元,就意味着我们抓住了事物的本事,再复杂的问题也会迎刃而解。 存储单元 存储器范围比较大…...

ssm125四六级报名与成绩查询系统+jsp
四六级报名与成绩查询系统的设计与实现 摘 要 互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&am…...

【Unity从零开始学习制作手机游戏】第01节:控制3D胶囊体运动
1. 新建Project L01 使用3D Mobile模板。 2. 建立一个平面,用来承载物体 3. 导入Unity库内的胶囊体 下载 StandardAssets https://download.unitychina.cn/download_unity/e80cc3114ac1/WindowsStandardAssetsInstaller/UnityStandardAssetsSetup-5.6.7f1.exe …...

内容安全(DPI和DFI解析)
内容安全前言: 防火墙的本质其实就是包过滤,我们通常所说的安全设备(如:IPS、IDS、AV、WAF)的检测重心是应用层。下一代防火墙基于传统防火墙的拓展能力,就是可以将以上的安全设备模块集成在一起࿰…...

2024数维杯数学建模A题B题C题思路+模型+代码(开赛后第一时间更新)
2024数维杯数学建模A题B题C题思路模型代码(开赛后第一时间更新) https://mbd.pub/o/bread/ZpWakpdq https://mbd.pub/o/bread/ZpWakpdq 2024年第九届数维杯大学生数学建模挑战赛参赛规则 竞赛要求及论文提交方式; ①本次参赛作品统一在线提交到竞赛…...

SpringSecurity多表,多端账户登录
本文章对应视频SpringSecurity6多端账号登录,可无限扩展教程,记得三连哦,这对我很重要呢! 温馨提示:视频与文章相辅相成,结合学习效果更强哦!更多视频教程可移步B站【石添的编程哲学】 SpringSe…...

绝地求生PUBG新老艾伦格有什么差别 老艾伦格什么时候回归
复古风格的艾伦格原始地图携带着那些标志性的记忆符号华丽回归,邀请您沉浸于往昔的每一处细节探索中。我们不仅还原了游戏诞生的起点,还在其中巧妙融入现代游戏元素,构筑一座连接昔日与今朝的桥梁,完美融合了经典与创新的游戏体验…...

Windows下安装Node.js、npm和electronic,并运行一个Hello, World!脚本程序
20240510 By wdhuag 目录 简介: 参考: 安装Node.js 安装npm 配置npm: 修改包存放目录和缓存目录 切换镜像源 使用 nrm 切换镜像源 安装Electron 运行一个Hello, World!脚本程序 安装Yarn JavaScript 指南 简介: Nod…...

【精品案例】化工炼化企业信息化建设解决方案(74页PPT)
一、资料介绍 化工炼化企业信息化建设解决方案是一份详尽且全面的指导文件,旨在助力化工炼化企业实现信息化、智能化和数字化转型。本资料以74页的PPT形式呈现,围绕智能化工程施工方案、化工炼化企业信息化以及化工行业数字化转型等关键词,为…...

【Unity Animation 2D】Unity Animation 2D骨骼绑定与动画制作
一、图片格式为png格式,并且角色各部分分离 图片参数设置 需要将Sprite Mode设置为Single,否则图片不能作为一个整体 1、创建骨骼 1.1 旋转Create Bone,点击鼠标左键确定骨骼位置,移动鼠标再次点击鼠标左键确定骨骼,…...

工器具管理(基于若依)
文章目录 前言一、工器具管理项目总览 二、入库功能1. 前端1.1 界面展示1.2 具体操作实现1.3 js文件 2. 后端2.1 工器具信息回显2.2 工器具入库 三、领用功能1. 前端1.1 界面展示1.2 具体实现操作1.3 js文件 2. 后端2.1 工器具信息回显2.2 工器具领用 遇到的问题1. 同一页面展示…...

UE4_照亮环境_光束light beam
学习笔记,不喜勿喷,侵权立删!祝愿生活越来越好! 光束:模拟大气中散射的光线。利用定向光源模拟真实曙暮光效果或大气散射的阴影,即可生成 光束 。这些光线为场景添加深度和真实度。 一:一些参数…...

springboot3项目练习详细步骤(第三部分:文章管理模块)
目录 发布文章 接口文档 业务实现 自定义参数校验 项目参数要求 实现思路 实现步骤 文章列表(条件分页) 接口文档 业务实现 mapper映射 更新文章 接口文档 业务实现 获取文章详情 接口文档 业务实现 删除文章 接口文档 业务实现 文章管理业务表结构…...

【面试八股总结】C++11新特性:智能指针
参考资料 :阿秀、代码随想录 智能指针是一个类,用于存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄露。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自…...

【教程向】从零开始创建浏览器插件(二)深入理解 Chrome 扩展的 manifest.json 配置文件
第二步:深入理解 Chrome 扩展的 manifest.json 配置文件 上一次我们已经着手完成了一个自己的浏览器插件,链接在这里:我是链接 在本篇博客中,我们将更详细地探讨 Chrome 扩展中的 manifest.json 文件。这个文件是每个浏览器扩展…...

美易官方:美国房地产贷款逾期率飙升,银行业危机仍可控?现货黄金暂守2360
美国房地产贷款逾期率飙升,银行业危机仍可控?现货黄金暂守2360 近年来,美国房地产市场一直处于波动之中,尤其是商业房地产领域。近期,美联储发出警告,称商业房地产贷款逾期率持续攀升,银行正在为…...

SwiftUI中的@StateObject和@ObservedObject的区别
SwiftUI中的StateObject和ObservedObject属性包装器指示视图更新以响应被观察对象的变化。虽然这两个属性包装器看起来很相似,但在使用SwiftUI构建应用程序时,有一个关键的区别需要理解。 两个属性包装器都要求对象符合ObservableObject协议。这个协议表…...

类与对象(二)
封装 封装作为面向对象三大特性(封装,继承,多态)之一,那如何实现封装性的呢?就又得拿出上面的访问修饰限定符的图 public: 就是在任何地方都可以访问 protected: 涉及子类在介绍继承时详细介绍 default: …...

LeetCode/NowCoder-链表经典算法OJ练习2
最好的,不一定是最合适的;最合适的,才是真正最好的。💓💓💓 目录 说在前面 题目一:分割链表 题目二:环形链表的约瑟夫问题 SUMUP结尾 说在前面 dear朋友们大家好!&…...

英伟达解码性能NVDEC
如果你能打开官网,请看这里: NVDEC Application Note 下面是摘录:...

文心一言 VS 讯飞星火 VS chatgpt (255)-- 算法导论18.3 1题
一、请说明依次从图18-8(f)中删除C、P和V后的结果。如果要写代码,请用go语言。 文心一言: 首先,让我们明确一点:由于您没有提供具体的图18-8(f)的内容,我只能假设它是一个数据结构(可能是图、树或列表等&…...

C++ | Leetcode C++题解之第73题矩阵置零
题目: 题解: class Solution { public:void setZeroes(vector<vector<int>>& matrix) {int m matrix.size();int n matrix[0].size();int flag_col0 false;for (int i 0; i < m; i) {if (!matrix[i][0]) {flag_col0 true;}for …...

用 Supabase CLI 进行本地开发环境搭建
文章目录 (零)前言(一)Supabase CLI(1.1)安装 Scoop(1.2)用 Scoop 安装 Supabase CLI (二)本地项目环境(2.1)初始化项目(2…...