C++虚表和虚基表

C++虚表和虚基表

文章目录

  • C++虚表和虚基表
    • 1.虚表/虚函数表(vtable)
      • 1.1概念
      • 1.2工作机制
      • 1.3虚表的特性
    • 2.虚基表(vbtable)
      • 2.1概念
      • 2.2工作机制
      • 2.3虚基表的特性
    • 3.深入认识虚基表
      • 3.1菱形继承对象模型
      • 3.2菱形虚拟继承对象模型
    • 4.深入认识虚表
      • 4.1含有虚函数的类对象模型
      • 4.2单继承中的虚表
      • 4.3多继承中的虚表
      • 4.4菱形继承中的虚表
    • 5.菱形虚拟继承中的虚表虚基表对象模型

1.虚表/虚函数表(vtable)

1.1概念

  • 虚表 是一种内部数据结构,用于支持多态。
  • 当一个类包含虚函数时,编译器会为该类生成一个虚表,以存储指向虚函数的指针。
  • 每个含有虚函数的类都有一个虚表,虚表用于记录类的虚函数地址,方便在运行时通过基类指针或引用调用派生类的实现。

1.2工作机制

  • 每个含有虚函数的类对象中都会有一个指向虚表的指针,称为 虚表指针(vptr)
  • 当通过基类指针调用虚函数时,编译器根据对象中的 vptr 找到相应的虚表,并从虚表中获得该虚函数的实际地址,保证调用正确的派生类函数。
  • 虚表在编译期生成,而 vptr 在对象创建时被初始化。

示例

class Base {
public:virtual void show() { std::cout << "Base show" << std::endl; }
};class Derived : public Base {
public:void show() override { std::cout << "Derived show" << std::endl; }
};

在这个例子中,BaseDerived 各自都有一个虚表,用于存储 show 函数的地址。当通过 Base* 指针调用 show 时,会通过虚表找到 Derived 中的 show 函数,实现多态。

1.3虚表的特性

  • 虚表是由编译器自动创建和维护的,开发者无法直接访问。
  • 对象的内存中通常包含一个指向虚表的指针(vptr)。
  • 虚表只在包含虚函数的类中存在。

2.虚基表(vbtable)

2.1概念

  • 虚基表 是编译器为了解决多重继承中的虚基类问题而引入的数据结构。
  • 它的目的是确保在多重继承中共享虚基类的唯一实例,从而避免重复继承带来的存储空间浪费和不一致的问题。

2.2工作机制

  • 当类通过多重继承包含同一个基类时,为了确保基类在派生类中只存在一个实例,可以将基类声明为虚基类。
  • 编译器会为包含虚基类的类生成一个虚基表,其中记录了虚基类实例的偏移量。
  • 虚基表(vbtable)通过这些偏移量来指示派生类如何访问唯一的虚基类实例。

示例

class A {
public:int value;
};class B : virtual public A {
};class C : virtual public A {
};class D : public B, public C {
};

在这个例子中,BC 都通过虚基类继承 A。因此 D 类中只会有一个 A 的实例。编译器会生成虚基表来记录 A 的地址偏移,以便 BC 都能正确访问 A 的成员。

2.3虚基表的特性

  • 只有在虚基类(virtual 基类)存在时才会生成虚基表。
  • 虚基表中存储了虚基类实例的偏移量。
  • 虚基表用于解决多重继承下的虚基类共享问题。

3.深入认识虚基表

研究环境:vs2022 32位平台

3.1菱形继承对象模型

class A {
public:int _a;
};class B : public A {
public:int _b;
};class C : public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};

上面是没有使用虚继承的菱形继承,导致D的对象成员里有两份A,导致了数据冗余二义性

图示

在这里插入图片描述

3.2菱形虚拟继承对象模型

使用虚继承:

class A {
public:int _a;
};class B :  public A {
public:int _b;
};class C :  public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};

图示

在这里插入图片描述

4.深入认识虚表

4.1含有虚函数的类对象模型

class A
{
public:virtual void func1() { cout << "A::func1" << endl; }int _a;
};

图示

在这里插入图片描述

4.2单继承中的虚表

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};

如何观察到类对象的虚表呢?我们设计函数来实现。

//定义函数指针类型,方便声明
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数// 指针的指针数组,这个数组最后面放了一个nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最//   后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再//	 编译就好了。VFPTR * vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

运行代码,我们得到

 虚表地址>00029B34第0个虚函数地址 :0X212b7,->Base::func1第1个虚函数地址 :0X21113,->Base::func2虚表地址>00029B64第0个虚函数地址 :0X21244,->Derive::func1第1个虚函数地址 :0X21113,->Base::func2第2个虚函数地址 :0X21230,->Derive::func3第3个虚函数地址 :0X2116d,->Derive::func4

发现:

  • 如果一个类通过单继承继承了一个基类,并且重写了基类的虚函数,那么派生类的虚表通常会直接覆盖基类的虚表。
  • 这种情况下,派生类会沿用基类的虚表布局,只是将重写的虚函数地址替换到对应的位置。
  • 因此,单继承的虚表是继承和延续的,不需要重新创建。

4.3多继承中的虚表

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}

发现:

  • 在多继承的情况下,派生类会从多个基类继承虚表。这种情况下,派生类的对象布局中会包含多个虚表指针(vfptr),每个指针对应一个基类的虚表。
  • 如果派生类重写了基类的虚函数或者定义了新的虚函数,则会为派生类创建一个新的虚表。这个新虚表不仅包含派生类自己的虚函数,还包含各个基类的虚函数(按照每个基类独立的虚表)。
  • 换句话说,多继承的派生类会拥有多个虚表,每个虚表分别管理来自不同基类的虚函数,以确保不同路径的虚函数调用可以正确绑定到对应的实现。

4.4菱形继承中的虚表

//在原先代码的基础上加入了Base0,让Base1和Base2对Base0进行public继承
class Base0
{
public:virtual void func1() = 0;
};
class Base1 : public Base0
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 : public Base0
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
  • Base0对Base1,Base2是单继承,Base1和Base2将沿用Base0的虚表。
  • Base1和Base2对Derive是多继承,Derive将会创建新的虚表

5.菱形虚拟继承中的虚表虚基表对象模型

class Base0
{
public:virtual void func1() = 0;
private:int b0 = 0;
};
class Base1 :  public Base0
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 1;
};
class Base2 :  public Base0
{
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 3;
};

图示

在这里插入图片描述

发现:

  • Base1和Base2本应该继承Base0的虚表,但是虚拟继承后,Base1和Base2创建自己的虚表。
  • 虚基表存的是偏移量

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全求职指南_看完这篇就足够了~

概述 之前的文章给大家分析了安全行业目前的发展趋势、安全防御和渗透攻击两端不同的技术栈需求。在这篇文章里面&#xff0c;我们聚焦以下常见的安全行业求职和职业发展问题&#xff1a; 安全行业如何区分&#xff1f;安全岗位到底有哪些&#xff1f;不同安全岗位的技术需求…

python: Parent-child form operations using ttkbootstrap

# encoding: utf-8 # 版權所有 2024 ©塗聚文有限公司 # 許可資訊查看&#xff1a;言語成了邀功的功臣&#xff0c;還需要行爲每日來值班嗎&#xff1f; # 描述&#xff1a; 主、子表單 窗體傳值 Parent-child form operations # Author : geovindu,Geovin Du 塗聚文. …

能让企业“脱胎换骨”的局域网电脑监控软件,有哪些?

老板们&#xff0c;是不是发现现在员工们在上班时间玩得那叫一个欢&#xff0c;而工作却被丢在一边&#xff1f;别愁啦&#xff01;今天就给各位带来一份超赞的局域网电脑监控软件指南&#xff0c;这就像是给企业配上了 “超级放大镜”&#xff0c;员工的一举一动都能看得清清楚…

什么是计算机视觉算法?——深度剖析背后的技术与应用

计算机视觉&#xff08;Computer Vision&#xff09;作为人工智能的重要分支&#xff0c;正在逐渐改变我们的生活。从人脸识别到自动驾驶&#xff0c;从医疗影像诊断到视频监控&#xff0c;计算机视觉的应用无处不在&#xff0c;而支撑这一切的正是计算机视觉算法。那么&#x…

产品如何3D建模?如何根据使用场景选购3D扫描仪?

随着科技的飞速发展&#xff0c;3D模型已从昔日的小众应用转变为各行各业不可或缺的利器。在文博、电商、家居、汽车、建筑及游戏影视等众多领域&#xff0c;3D模型以其直观、真实的视觉体验发挥着至关重要的作用。它不仅使用户能深入了解产品的外观、结构与功能&#xff0c;还…

信息安全工程师(79)网络安全测评概况

一、定义与目的 网络安全测评是指参照一定的标准规范要求&#xff0c;通过一系列的技术、管理方法&#xff0c;获取评估对象的网络安全状况信息&#xff0c;并对其给出相应的网络安全情况综合判定。其对象主要为信息系统的组成要素或信息系统自身。网络安全测评的目的是为了提高…

Windows 系统上配置 SSH 密钥验证,实现无密码登录

Windows 系统上配置 SSH 密钥验证&#xff0c;实现无密码登录 在日常工作中&#xff0c;使用密码登录远程云服务器往往让人感到繁琐。云服务器的密码通常较长&#xff0c;难以记忆&#xff0c;每次登录都需要反复输入&#xff0c;既不便捷也影响效率。此外&#xff0c;由于网络…

俯仰 (pitch) 偏摆 (yaw) 翻滚 (roll)

pitch()&#xff1a;俯仰&#xff0c;将物体绕X轴旋转&#xff08;localRotationX&#xff09; yaw()&#xff1a;航向&#xff0c;将物体绕Y轴旋转&#xff08;localRotationY&#xff09; roll()&#xff1a;横滚&#xff0c;将物体绕Z轴旋转&#xff08;localRotationZ&…

es数据同步(仅供自己参考)

数据同步的问题分析&#xff1a; 当MySQL进行增删改查的时候&#xff0c;数据库的数据有所改变&#xff0c;这个时候需要修改es中的索引库的值&#xff0c;这个时候就涉及到了数据同步的问题 解决方法&#xff1a; 1、同步方法&#xff1a; 当服务对MySQL进行增删改的时候&…

从0开始学习Linux——Yum工具

往期目录&#xff1a; 从0开始学习Linux——简介&安装 从0开始学习Linux——搭建属于自己的Linux虚拟机 从0开始学习Linux——文本编辑器 上一个章节我们简单了解了Linux中常用的一些文本编辑器&#xff0c;本次教程我们将学习yum工具。 一、Yum简介 Yum&#xff08;全名…

高级AI记录笔记(一)

学习位置 B站位置&#xff1a;红豆丨泥 UE AI 教程原作者Youtube位置&#xff1a;https://youtu.be/-t3PbGRazKg?siRVoaBr4476k88gct素材自备 提前将动画素材准备好 斧头蓝图 斧头武器插槽 混合空间 就是改了一下第三人称模版的动画蓝图 行为树中不用Wait实现攻击完…

ffmpeg的下载与安装

废话不多说&#xff0c; 下载地址&#xff0c;得找官网&#xff0c;不然得注意是不是有夹带私活。 FFmpeg 这个是目前的最新版本&#xff1b; 下载的时候看下自己要的版本&#xff0c;我的是Windows10&#xff1b; 解压后的版本长这样&#xff1a; 接下来进行环境变量的配置&…

【http协议笔记】-- 浏览器简单分析get、post请求

环境&#xff1a;为了了解http协议的交互方式&#xff0c;使用edge浏览器简单分析协议内容&#xff0c;给刚入门的小伙伴分享一下&#xff0c;方便大家学习。 以菜鸟教程的网站为例子&#xff1a; 分析post&#xff1a; 请求url&#xff1a; 请求参数&#xff1a; 请求相应&a…

SpringBoot【实用篇】- 热部署

文章目录 目标:1.手动启动热部署2.自动启动热部署4.禁用热部署 目标: 手动启动热部署自动启动热部署热部署范围配置关闭热部署 1.手动启动热部署 当我们没有热部署的时候&#xff0c;我们必须在代码修改完后再重启程序&#xff0c;程序才会同步你修改的信息。如果我们想快速查…

vue3相对vue2有哪些改变?

https://blog.csdn.net/weixin_44475093/article/details/112386778 https://blog.csdn.net/userDengDeng/article/details/114941956 一、vue3的新特性&#xff1a; 1、速度更快 vue3相比vue2 重写了虚拟Dom实现编译模板的优化更高效的组件初始化undate性能提高1.3~2倍SSR速度…

数据库概论实验一

声明&#xff1a;著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 本文章对数据库概论实验一_求出一箱(每箱装100个)零件的重量 并将输出结果-CSDN博客文章浏览阅读2.7k次&#xff0c;点赞4次&#xff0c;收藏25次。实验数据库&#xff0c;表…

绿色能源发展关键:优化风电运维体系

根据QYResearch调研团队最新发布的《全球风电运维市场报告2023-2029》显示&#xff0c;预计到2029年&#xff0c;全球风电运维市场的规模将攀升至307.8亿美元&#xff0c;并且在接下来的几年里&#xff0c;其年复合增长率&#xff08;CAGR&#xff09;将达到12.5%。 上述图表及…

gerrit 搭建遇到的问题

1、启动Apache&#xff0c;端口被占用 : AH00072: make sock: could not bind to address (0S 10048)通常每个套接字地址(协议/网络地址/端口)只允许使用一次。: AH00072: make sock: could not bind to address 0.0.0.:443 a AH00451: no listening sockets available, shutti…

栈和队列相关题 , 用队列实现栈, 用栈实现队列 ,设计循环队列 C/C++双版本

文章目录 1.用队列实现栈2.用栈实现队列3. 设计循环队列 1.用队列实现栈 225. 用队列实现栈 思路&#xff1a; 使用两个队列&#xff0c;始终保持一个队列为空。 当我们需要进行压栈操作时&#xff0c;将数据压入不为空的队列中&#xff08;若两个都为空&#xff0c;则随便压…

关于STM32在代码中的而GPIO里面的寄存器(ODR等)不需要宏定义的问题

1.GPIO为什么需要宏定义地址 在 STM32 这样的微控制器中&#xff0c;硬件寄存器的地址是固定的并且特定于每个外设&#xff08;比如 GPIOA、GPIOB 等&#xff09;。为了方便代码访问这些硬件寄存器&#xff0c;我们通常会使用宏定义来指定每个外设的基地址。这样做有几个理由&a…