C++的虚基类

前言

本文介绍C++的虚基类

先看一个问题

先看一段代码

#include <iostream>
class A
{
public:int a = 1;
};class B1:public A
{
public:int b1 = 2;
};class B2 :public A
{
public:int b2 = 3;
};class C1:public B1,public B2
{
public:int c1 = 4;
};int main(int argc, const char* argv[])
{C1 c1;std::cout << "C1 size::" << sizeof(c1) << std::endl;return 0;
}

打印c1的大小,发现c1的大小是20字节,我们猜测一下这20个字节应该是

  • C1类中的c1,int类型占用四个字节
  • B1类中的b1,int类型占用四个字节
  • B1类继承A类中的a,int类型占用四个字节
  • B2类中的b2,int类型占用四个字节
  • B2类继承A类中的a,int类型占用四个字节

我们通过下面添加下面语句打印c1的地址

std::cout << "c1 pointer::" << &c1 << std::endl;

我这里获取到c1的内存地址为0x000000D9887FF958,然后使用这个地址去内存界面(调试–窗口–内存–内存1)中查找(必须在程序运行过程中才能查看内存数据),结果如下:
在这里插入图片描述

因为我们给变量都赋值了,可以通过取值区分变量,我们能够得到c1的内存布局如下:

  • a
  • b1
  • a
  • b2
  • c

那下面问题来了,我们如果想给c1中的a赋值会发生什么呢?因为c1在内存中有两个a,编译器无法确定a的偏移地址应该是哪个,所以如果操作c1中的a,编译器会报错:C1::a不明确
确实不明确。

怎么解决这个问题呢?C++通过使用虚基类解决这种问题,本文重点介绍

虚继承

定义

当一个类在继承的基类前面添加关键字virtual时,我们就说这个类从基类虚继承,看下面的代码:

#include <iostream>
class A
{
public:int a = 1;
};// 注意关键字virtual
class B1:virtual public A
{
public:int b1 = 2;
};

类B1就叫做从A虚继承,A在被虚继承的情况下被称为虚基类,注意,虚基类是有条件的,只有在被虚继承的时候才是虚基类

虚继承的特点

对于虚继承的类,上面的例子就是B1,编译器会在类的成员变量里面添加一个指针,这个指针叫做虚基类表指针,简称vbptr,全称virtual base pointer,该指针指向一个虚基类表,简称vbtable,全称virtual base table
我们打印一下B1类实例的大小:

int main(int argc, const char* argv[])
{B1 b1;std::cout << "b1 pointer::" << &b1 << std::endl;std::cout << "b1 size::" << sizeof(b1) << std::endl;return 0;
}

运行结果发现b1的大小足足有24个字节,怎么会这么大呢,我们又打印出b1在内存中的地址,我这里是0x000000AC17EFFC08,去内存界面查找结果如下:
在这里插入图片描述

因为我们给变量都赋了值,所以比较容易查看,b1在内存的布局如下:

  • 一个指针,占8个字节,因为我电脑是64位的
  • b1的值,注意,这里不是a1,这跟常规的继承似乎不太一样,占用四字节
  • 四字节的对齐位
  • a的值。占用四字节
  • 四字节的对齐位,正好24个字节

首位的那个指针就是我们前面说的虚函数表指针。

然后我们继续添加类C1继承B1

class C1:public B1
{
public:int c1 = 4;
};

观察C1实例的大小和内存布局如下:
在这里插入图片描述

  • 一个指针,占8个字节,因为我电脑是64位的
  • b1的值,注意,这里不是a1,这跟常规的继承似乎不太一样,占用四字节
  • 四字节的对齐位
  • c1的值。占用四字节
  • 四字节的对齐位
  • a的值。占用四字节
  • 四字节的对齐位,32个字节

我们发现,对于多层继承关系,虚基类的成员变量始终放在最后

虚基类表

虚基类表的创建时机

对于虚继承,编译器在编译期间就已经生成了虚基类表,一个虚继承的类对应一个虚基类表,这点和包含虚函数的类一样,一个包含虚函数的类对应一个虚函数表

虚基类表的内容

虚基类表中保存的不是指针,这一点和虚函数表不同,虚基类表中保存的是偏移量,是int,什么偏移量呢?继续往后看!

我们重新修改一下代码,让C1同时虚继承自B2和A:

#include <iostream>
class A
{
public:int a = 1;
};class B1
{
public:int b1 = 2;void b1_func() {};
};class B2
{
public:int b2 = 3;void b2_func() {};
};class C1:virtual public B2, public B1, virtual public A
{
public:int c1 = 4;void c1_func() { b2_func(); };
};

然后使用vs的命令行工具(通常在开始菜单–visual studio 20xx文件夹–Developer Command Prompt for VS 20xx)
使用cd命令切换到当前工程目录下,使用下面的命令:

cl /d1 reportSingleClassLayoutC1 main.cpp

注意C1是打印的类名,main.cpp是类所在的文件名
回车以后可以得到C1的布局信息,信息如下:
在这里插入图片描述

可以看到在C1的虚基类表中有三条信息,比虚继承的类的数目多1,经过我们使用不同数量的虚继承类进行测试,得到如下结论:

  • 一个虚基类表中的表项数目等于虚继承的类的数目加1
  • 从虚基类表的第二项开始,表示虚基类与虚基类表指针的偏移量。第二项表示虚继承的第一个类,第三项表示虚继承的第二个类,依次类推。。。

到目前为止,我们有两个疑问?

  1. 为什么记录偏移?
  2. 第一项是个什么玩意?

为什么记录偏移

为什么要定义偏移呢,因为假如我们现在要访问B2类中的函数

void b2_func(){}

在这个函数中,可能有访问B2类某个变量的操作,或者给B2的某个变量赋值,我们知道,C++是通过在成员函数中插入this指针参数来达到这个目的的。既然成员函数在编译期间就已经编译完成了,也就是代码已经写好了,那么我们传递的this值必须指向真正的B2的位置才行,不然通过this地址+偏移寻找成员变量的操作就会失败。那么怎么才能找到真正的B2的位置呢?毕竟我们现在只有C1的位置。

这就是虚基类表的作用,通过从虚基类表中获取对应虚基类的偏移,然后通过下面的公式获取虚基类的真实地址:
B2的真实地址 = C1的地址+虚基类表指针的偏移+虚基类的偏移

下面是访问该函数的汇编代码:

 c1->b2_func();
00007FF6975F235E  mov         rax,qword ptr [c1]  
00007FF6975F2362  mov         rax,qword ptr [rax+8]  
00007FF6975F2366  movsxd      rax,dword ptr [rax+4]  
00007FF6975F236A  mov         rcx,qword ptr [c1]  
00007FF6975F236E  lea         rax,[rcx+rax+8]  
00007FF6975F2373  mov         rcx,rax  
00007FF6975F2376  call        B2::b2_func (07FF6975F1203h) 
  • 第一行将c1地址的值传给rax寄存器
  • 第二行是通过获取虚基类表指针的内容将虚基类表的地址传给rax寄存器
  • 第三行将虚基类表的第二项的值传递给rax寄存器
  • 第四行将c1地址的值传给rcx寄存器
  • 第五行将(c1地址值+虚基类表指针的偏移+虚基类表的第二项的值)的传给rax寄存器,lea是取地址指令,就是获取当前地址的值,而不是地址的内容。经过这步,rax寄存器的值给rcx寄存器,这一步是参数的传递保存的是B2真正的地址
  • 第六行将rax寄存器的值给rcx寄存器,这一步是参数的传递
  • 第七行开始函数的调用

我们可以总结,虚基类表的偏移是为了能够对虚基类进行操作

关于第一项

那么虚基类表的第一项呢?
为了了解第一项的值,我们得先运行一下代码,因为运行代码以后内存中的数据布局和上面控制台打印的类布局结构是不一样的,因为类布局结构并没有考虑边界对齐,所以我们给出上面代码运行时的内存数据,先给出c1的内存布局:
在这里插入图片描述

然后根据c1中虚基类表指针的值再去找到虚基类表的内存:
在这里插入图片描述

可以看到虚基类表的前三项分别为:

  • fffffff8:-8
  • 00000010:16
  • 00000014:20

这样我们结合自己的类的定义就大体知道第一项的值代表虚基类表指针到拥有当前指针的类地址的偏移。

三层虚继承

到目前为止,我们都是分析虚继承,事实上,虚基类的应用至少需要三层,别忘了虚基类的目的是什么,是为了保证基类在类布局中只保留一份。两层的时候是没有同一个基类出现多次的情况的。
保留上面的代码,新添加一个类C2,和C1有同样的继承关系,新建一个类D1,同时继承C1和C2:

class C2 :virtual public B2, public B1, virtual public A
{
public:int c2 = 5;void c2_func() { b2_func(); };
};class D1 :public C1, public C2
{
public:int d1 = 6;
};

到这里先暂停一下,再来回顾一下虚基类的概念:

  • 虚基类的最终目标是存在多个时只保留一份,并且放在布局的最后面
  • 虚基类表是针对于虚继承的类的,不是虚基类
  • 虚基类表指针存在于虚继承的类,是类的一部分,通常放在类的前面,虚基类没有这玩意

好,回到例子,我们打印一下D1实例的大小,然后看一下D1的布局:
D1实例的大小是64字节,布局如下,注意,布局没有考虑边界对齐:
在这里插入图片描述

我们分析一下布局的情况:

  • D1:因为D1先继承C1,并且不是虚继承,所以先排列C1的数据
    • C1:因为 C1虚继承B2和A,所以虚基类B2和A扔到最后,这里先不管,只知道C1有一个虚函数表指针,又因为C1还继承自B1,所以要先排列B1
      • B1:b1=2 //目前4字节
      • B1:边界对齐 //目前8字节
    • B1排列完之后,C1没有别的继承了,开始排列自己
    • C1:vbtable,先排列虚基类表指针 //目前16字节
    • C1:c1=4 //目前20字节
    • C1:边界对齐 //目前24字节
  • D1:C1的排列完成后,开始排列C2,C2和C1结构一样,可以跳过往后看
    • C2:因为 C2虚继承B2和A,所以虚基类B2和A扔到最后,这里先不管,只知道C2有一个虚函数表指针,又因为C2还继承自B1,所以要先排列B1
      • B1:b1=2 //目前28字节
      • B1:边界对齐 //目前32字节
    • B1排列完之后,C2没有别的继承了,开始排列自己
    • C2:vbtable,先排列虚基类表指针 //目前40字节
    • C2:c2=5 //目前44字节
    • C2:边界对齐 //目前48字节
  • D1:继承的类都排列完成,开始排列自身的成员
  • D1:d1=6 //目前52字节
  • D1:边界对齐 //目前56字节
  • D1:继承的类都排列完成,开始排列虚基类,虚基类的排列按照继承关系出现的先后顺序排列,对于当前例子,先排列B2,再排列A
    • B2:b2=3 //目前60字节
    • A:a=1 //目前64字节

下面是内存的数据:
在这里插入图片描述

根上面的分析刚好一致。

一共有两个虚基类表,一个偏移位置为8,一个偏移位置为32
所以基类到两个虚基类表的偏移为:

  • 偏移位置为8:
    • B2:48
    • A:52
  • 偏移位置为32:
    • B2:24
    • A:28

然后查看第一个虚基类表指针00007ff6468fbc38对应的虚基类表的内存数据:
在这里插入图片描述

可以看到虚基类表的前三项分别为:

  • fffffff8:-8
  • 00000030:48
  • 00000034:52
    与结果一致

然后查看第二个虚基类表指针00007ff6468fbdb8对应的虚基类表的内存数据:
在这里插入图片描述

可以看到虚基类表的前三项分别为:

  • fffffff8:-8
  • 00000018:24
  • 0000001c:28
    与结果一致

如果包含虚函数呢

如果一个类既包含虚函数表又包含虚基类表的情况下,应该是怎么排列的呢?
看下面的简单代码:

class A
{
public:int a = 1;
};class C1:virtual public A
{
public:int c1 = 4;virtual void c1_func() { };
};

C1包含虚函数,所以应该有一个虚函数表指针,又虚继承A,所以应该有一个虚基类表指针,我们打印一下C1的布局:
在这里插入图片描述

可以看到,先排列虚函数表指针,在排列虚基类表指针

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日汇评:黄金多头能否在美国CPI数据发布后占有主动权?

黄金价格再次在2020美元附近找到支撑&#xff0c;因为所有人都在关注美国的通胀数据&#xff1b; 尽管最近美国国债收益率有所上升&#xff0c;但美元仍进一步下跌&#xff1b; 金价保持在21日移动均线和50日移动均线之间&#xff0c;等待区间突破&#xff1b; 在周四早盘的亚洲…

SIT1050ISO具有隔离功能,1Mbps,高速 CAN 总线收发器

➢ 完全兼容“ ISO 11898 ”标准&#xff1b; ➢ 内置过温保护&#xff1b; ➢ 100kV/s 瞬态抗扰度&#xff1b; ➢ 显性超时功能&#xff1b; ➢ -40V 至 40V 的总线故障保护&#xff1b; ➢ I/O 电压范围支持 3.3V 和 5V MCU &#xff1b; ➢ 低环路延迟…

解决原生微信小程序获取关联公众号的code(不是wx.login的code)来获取公众号的openId

解决步骤 以下是使用 web-view 并配配合微信公众号提供的 网页授权 来实现 1、在小程序中做一个web-view页面&#xff0c;页面中只需要写微信 网页授权的链接就行了&#xff0c;注意appid请自行替换&#xff08;公众号的&#xff09;。 onLoad() {this.setData({src: https://o…

Android SDK环境搭建[图解]; 解决问题Done. Nothing was installed.

安装SDK Android SDK环境搭建 依赖java环境,需要自备Java环境 (100%实操成功) 目录 1. 解压&#xff1a;解压到非中文无特殊字符的目录 2. 双击&#xff1a;SDK Manager.exe&#xff0c;不要选全部!不要选全部!不要选全部!(会下很久) 3. 然后勾选组件​ 4. 设置环境变量 …

UM2003A 一款200 ~ 960MHz ASK/OOK +18dBm 发射功率的单发射芯片

UM2003A 是一款工作于 200 ~ 960MHz 频段的单片集成、高性能、可独立运行的 OOK 发射器。内部集成的 OTP 方便用户对各种射频参数以及特色功能进行编程。该芯片以其高集成度和低功耗的设计&#xff0c;特别适用于低成本&#xff0c;低功耗&#xff0c;电池驱动的无线发射应用。…

【TypeScript】入门基础知识

目前在做项目的技术栈是 reacttypescript&#xff0c;之前只知道 ts 是 js 的扩展&#xff0c;增加了类型检查&#xff0c;但是没有仔细的学过&#xff0c;纯纯看别人代码上手 anyscript&#xff08;这很难评...&#xff09;。趁着最近空闲&#xff0c;就学习一下 ts 的基础知识…

章鱼网络 2023 年全回顾|暨12月进展报告

2023年&#xff0c;章鱼网络轻装上阵&#xff0c;身处加密行业的低谷中砥砺前行。 12月17日&#xff0c;经过整整1年时间的开发和打磨&#xff0c;章鱼网络在重磅上线 Octopus 2.0&#xff0c;即 $NEAR Restaking 和 NEAR-IBC&#xff0c;获得了社区和市场的一致认可&#xff…

Java中多线程二

抢占调度模型 概述&#xff1a;优先让优先级高的线程使用 CPU &#xff0c;如果线程的优先级相同&#xff0c;那么随机会选择一个&#xff0c;优先级高的线程获取的 CPU 时间片相对多一些 Thread 类中一些关于线程的方法 方法简述public final int getPriority()返回此线程的优…

自动化控制面板-1Panel

一、1Panel自动化控制面板 官网地址 1Panel 可以实现&#xff1a; 快速建站、高效管理、安全可靠、一键备份、应用商店 快速建站&#xff1a;深度集成 Wordpress 和 Halo&#xff0c;域名绑定、SSL 证书配置等一键搞定&#xff1b;高效管理&#xff1a;通过 Web 端轻松管理 …

Docker启动报错:No chain/target/match by that name 处理

一、问题描述 某次OS升级重启后&#xff0c;发现docker redis实例无法启动&#xff0c;报错如下&#xff1a; Error response from daemon: driver failed programming external connectivity on endpoint vpm.redis.2 (f4b70fef65000bcacb574ee59e65d9b7a25f2abfa5dec0be9b74…

阿里云实时计算企业级状态存储引擎 Gemini 技术解读

本文整理自阿里云 Flink 存储引擎团队李晋忠&#xff0c;兰兆千&#xff0c;梅源关于阿里云实时计算企业级状态存储引擎 Gemini 的研究&#xff0c;内容主要分为以下五部分&#xff1a; 流计算状态访问的痛点企业级状态存储引擎GeminiGemini 性能评测&线上表现结语参考 一、…

2024年最新ChemiCloud优惠75%折扣WordPress外贸主机

ChemiCloud怎么样&#xff1f;ChemiCloud好不好&#xff1f;ChemiCloud是一家成立于2016年的云虚拟主机提供商&#xff0c;他们在全球范围内拥有多个机房&#xff0c;并以其出色的性价比而备受赞誉。他们整合了许多先进技术&#xff0c;包括Digital Ocean SSD云服务器、LiteSpe…

云计算任务调度仿真03

前面陆续分享了基于policy gradient和DQN实现的深度强化学习任务调度仿真&#xff0c;上次的DQN没有实现fix-qtarget和experience replay&#xff0c;这次再分享实现了这两个方法的DQN任务调度仿真。 经验重放&#xff0c;定义存储和存放次序&#xff0c;这里也可以自行修改 de…

提升测试效率,轻松并行运行测试——探秘Pytest插件pytest-xdist

在软件开发中&#xff0c;测试是确保代码质量的重要一环。然而&#xff0c;随着项目规模的增大&#xff0c;测试用例的数量也随之增多&#xff0c;测试的执行时间可能成为一个瓶颈。为了解决这个问题&#xff0c;Pytest提供了丰富的插件生态系统&#xff0c;其中 pytest-xdist …

[C#]调用tesseact-ocr的traineddata模型进行ocr文字识别

【框架地址】 https://github.com/charlesw/tesseract 【算法介绍】 Tesseract OCR是一个开源的光学字符识别引擎&#xff0c;它可以将图像中的文字转换成可编辑和可搜索的文本格式。Tesseract由惠普实验室于1985年开始开发&#xff0c;并在2005年被Google收购后成为了开源项…

api网关-kong

选型 api网关相关功能 服务的路由 动态路由负载均衡 服务发现 限流 熔断、降级 流量管理 黑白名单反爬策略 控制台&#xff1a;通过清晰的UI界面对网关集群进行各项配置。 集群管理&#xff1a;Goku网关节点是无状态的&#xff0c;配置信息自动同步&#xff0c;支持节点水…

7.云原生之jenkins集成SonarQube

1. 私有云实战之基础环境搭建 2. 云原生实战之kubesphere搭建 3.云原生之kubesphere运维 4. 云原生之kubesphere基础服务搭建 5.云原生安全之kubesphere应用网关配置域名TLS证书 6.云原生之DevOps和CICD 7.云原生之jenkins集成SonarQube 8.云原生存储之Ceph集群 文章目录 搭建 …

BGP公认任意属性——MED(二)

BGP公认任意属性有两个&#xff0c;分别是&#xff1a;Local-preference 和 MED&#xff0c;本期介绍MED。 点赞关注&#xff0c;持续更新&#xff01;&#xff01;&#xff01; MED 特点 MED &#xff08;多出口鉴别器&#xff09;&#xff0c;也称为BGP COST&#xff0c;…

usb静电防护芯片选择

方案1 USBLC6-2SC6 优缺点 优点&#xff1a;进出使用不同的焊盘&#xff0c;如果没有焊接好信号必定不能通过。有效的避免了虚焊导致故障。 缺点&#xff1a;不能省略&#xff0c;调试时也不能省略。 原理图 参考价格 参考来源 USB切换方案&#xff0c;多电脑共用USB方案…

高级路由学习试题

文章目录 高级路由学习试题一.高级路由题目答案 二.OSPF 相关答案 三.基础知识答案 高级路由学习试题 一.高级路由题目 1.以下属于ITOIP特性的有&#xff08;&#xff09; A、智能 B、开放 C、融合 D、标准 2.层级化网络模型将网络划分为&#xff08;&#xff09; A、汇…