当前位置: 首页 > news >正文

【C++】-多态的底层原理

> 提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、虚函数表
  • 二、多态的原理
  • 三、解决疑惑
  • 四、多继承中的虚函数表
  • 五、总结


前言

今天我们开始讲解多态的底层原理,相信这篇博客会让你对多态的理解会更加的透彻,话不多说我们开始进入讲解


一、虚函数表

我们在多态语法的时候一直强调要构成虚函数,我们来看看虚函数在内存是怎么存储的?

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};

在这里插入图片描述
我们发现我们对象里面存放两个内容:成员变量和一个指针地址,刚好大小就是8。====

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析


我们往这这个Base里面增加先的内容:

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

在这里插入图片描述

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,成员包括自身的变量和虚表里面的虚函数。还有一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    在这里插入图片描述

但是最好每一次都重新生成一下解决方案,不然你在是之前的基础上修改在调试的可能就看不到效果

  1. 有虚函数的类创建不同对象,共有的是同一张虚表
    在这里插入图片描述

  2. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

在这里插入图片描述

我们猜想这可能是vs监视窗口的一个bug,一会验证内存当中多出来的地址是不是我们特有的虚函数。

二、多态的原理

为什么需要的是基类的指针和引用调用虚函数

(1)为什么是基类的??
我们在继承的第二节讲到,继承的赋值,子类对象可以赋值给父类的对象指针和引用,父类可以接收自身的,也可以接收子类的,而子类只能接收自己的,如果是强转,可能会造成一系列问题,所以多态规定只能是基类的
(2)为什么是指针和引用去调用?
在这里插入图片描述>是将地址赋值给父类指针变量,引用底层也就是指针道理是一样的,指向什么对象就去调用什么哪个对象的函数了,这样就实现同一行为,展现出不同的形态
3)为什么虚函数要进行重写
如果不重写,就达不到覆盖的效果,那么子类的虚表还是存的是父类里面的虚函数,虽然你指向子类的虚表,但是虚表里面指向的函数地址还是父类的虚函数,重写了就完成了覆盖,重写就相当于对父类虚函数的重新定义放在了子类的虚表里面了。

那么对象为什么不行??

在这里插入图片描述原因是对象赋值时要调用拷贝构造或者赋值运算符的,子类会进行切片将父类的那一部分拷贝给父类对象,此时子类的虚表指针如果也拷贝过去了,会影响父类对象里面的虚表指针,那样就乱套了,指针和引用并不会改变父类对象里面的变量和虚表指针的。所以就不允许这样的赋值,就算拷贝过来,也是属于父类里面本身的属性拷贝过来,就把d1里面的_b给拷贝过去了,虚表还是父类本身的虚表,那么调用的时候就还是父类的函数,就调不到子类的。

三、解决疑惑

1. 虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

证明一下:
我们猜想有四个位置:栈,堆,数据段(静态区),代码段(常量区)
我们通过代码来演示:

在这里插入图片描述
通过测试我们发现虚表的地址离常量区最近,也就是代码段,有的书上说是在静态区,但是自己测试之后才知道应该离常量区最近。

2. 为什么监视窗口没有特有的虚函数,内存当中多出来的地址是不是我们猜想的结果:

在上面第二节的第六小点,我们发现,子类特有的属性居然不在虚表里面,二内存中却多出来了一个地址,我们猜测是那个特有的虚函数,但是也不能确定,所以我们只能想办法验证:
在这里插入图片描述
我们需要写一个函数,将函数数组指针里面的地址取出来,然后再调用

在这里插入图片描述
我们确实把地址取出来,接下来直接通过地址来调用:
在这里插入图片描述

确实和我们猜想的是一样的

测试代码:

class A
{
public:virtual void fun1() { cout << "A::fun1" << endl; }
};
class B :public A
{
public:virtual void fun1() { cout << "B::fun1" << endl; }virtual void fun2(){ cout << "B::fun2" << endl; }
};typedef void(*Fun_c)();void Adderss(Fun_c arr[])
{for (int i = 0; arr[i] != nullptr; i++){printf("[第%d个地址]:%p\n", i + 1, arr[i]);arr[i]();}
}
int main()
{B b;
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。Adderss((Fun_c*)(*(int*)&b));return 0;
}

四、多继承中的虚函数表

刚才我们说的都是单继承中的,把单继承的原理搞懂,多继承的处理方法其实思路是一样的,有虚表,但是因为是多继承,继承下来的不是一个类的成员,所以再处理方面还是有所不同的,接下来我来给大家介绍,我们的多继承的虚表是什么样的
我们来看测试代码:

class A
{
public:virtual void funa1() { cout << "A::funa1" << endl; }virtual void funa2() { cout << "A::funa1" << endl; }//基类特有的虚函数
};
class B
{
public:virtual void funb1(){ cout << "B::funb1" << endl; }virtual void funb2() { cout << "B::funb1" << endl; }//基类特有的虚函数
};
class C :public A,public B
{
public:virtual void funa1() { cout << "C::funa1" << endl; }//重写A类的虚函数virtual void funb1(){ cout << "C::funb1" << endl; }//重写B类的虚函数virtual void func1() { cout << "C::func1" << endl; }//派生类特有的虚函数void func2() { cout << "C::func2" << endl; }//不是虚函数
};
int main()
{C c;return 0;
}

在这里插入图片描述
通过上面图的结果来看,我们多继承的派生类中有两张虚表,而且猜想派生类特有的虚函数是放在第一张表中,此时我们还是按照上面方法去验证:
在这里插入图片描述

果然和我们猜想是一样的

我们再来看看下面的案例:

class A
{
public:virtual void fun1() { cout << "A::fun1" << endl; }virtual void fun2() { cout << "A::fun2" << endl; }
};
class B
{
public:virtual void fun1(){ cout << "B::fun1" << endl; }virtual void fun2() { cout << "B::fun2" << endl; }};
class C :public A,public B
{
public:virtual void fun1() { cout << "C::fun1" << endl; }virtual void fun3(){ cout << "C::fun3" << endl; }//特有的虚函数
};int main()
{C c;//因为C类的一个fun1是重写了A和B类的虚函数,所以指向谁就调用谁A* a = &c;a->fun1();B* b = &c;b->fun1();return 0;
}

在这里插入图片描述
通过这个案例我们又可以猜想,是不是地址实际就一个,其中以恶搞是直接找到的,另一个做了一下修改最后也能找到,因为实际想想同一分代码用两个地址存,显然有点浪费空间了,所以编译器也不允许这样的事情发生,带着这个疑问,我们通过汇编来看看是什么样的:
在这里插入图片描述

因为fun1的真正地址只有一份,有三种调用fun1的方式,其中两种就是上面画图演示的多态调用,还有一种是c对象自己去调用,而多态调用是去虚表中找,自己就直接调用,最终都需要指向c对象才能去调用,a和c对象的指向刚好重叠了,所以也类似于直接调用,而b对象需要修正,才能去调用。

上面我们说的都不是菱形继承的多继承,前面也说过,尽量不要设计出菱形继承,所以我们去研究也没有什么意义,所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

五、总结

今天讲解的知识还是以往大家下来自己去测试一样,尽量在测试前清理一下解决方案,不然会有影响。相信大家知道了底层原理之后,对于多态的时候应该不在陌生了,就是由于这一系列的底层要求,多态的形成条件才有那么多,也明白了为什么要哪些条件了,这篇博主花了很长时间,帮助自己梳理了一遍知识,也把知识分享给大家啊,希望大家多多支持
请添加图片描述

相关文章:

【C++】-多态的底层原理

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …...

【部署】让你的电脑多出一个磁盘来用!使用SSHFS将远程服务器目录挂载到Windows本地,挂载并共享服务器资源

让你的电脑多出一个磁盘来用&#xff01;---使用SSHFS将远程服务器目录挂载到Windows本地 1. 方法原理介绍2.SSHFS-Win使用教程—实现远程服务器磁盘挂载本地 由于日常主要用 Windows 系统&#xff0c;每次都得 ssh 到服务器上进行取资源&#xff08;本地磁盘不富裕&#xff09…...

/var/lock/subsys目录的作用

总的来说&#xff0c;系统关闭的过程&#xff08;发出关闭信号&#xff0c;调用服务自身的进程&#xff09;中会检查/var/lock/subsys下的文件&#xff0c;逐一关闭每个服务&#xff0c;如果某一运行的服务在/var/lock/subsys下没有相应的选项。在系统关闭的时候&#xff0c;会…...

DETR (DEtection TRansformer)基于自建数据集开发构建目标检测模型超详细教程

目标检测系列的算法模型可以说是五花八门&#xff0c;不同的系列有不同的理论依据&#xff0c;DETR的亮点在于它是完全端到端的第一个目标检测模型&#xff0c;DETR&#xff08;Detection Transformer&#xff09;是一种基于Transformer的目标检测模型&#xff0c;由Facebook A…...

C++初阶 - 5.C/C++内存管理

目录 1.C/C的内存分布 2.C语言中动态内存管理方式&#xff1a;malloc、calloc、realloc、free 3.C内存管理方式 3.1 new/delete操作内置类型 3.2 new 和 delete操作自定义类型 4.operator new 与 operator delete 函数&#xff08;重要点&#xff09; 4.1 operator new 与…...

数学建模学习(3):综合评价类问题整体解析及分析步骤

一、评价类算法的简介 对物体进行评价&#xff0c;用具体的分值评价它们的优劣 选这两人其中之一当男朋友&#xff0c;你会选谁&#xff1f; 不同维度的权重会产生不同的结果 所以找到每个维度的权重是最核心的问题 0.25 二、评价前的数据处理 供应商ID 可靠性 指标2 指…...

【后端面经】微服务构架 (1-5) | 限流:濒临奔溃?限流守护者拯救系统于水火之中!

文章目录 一、前置知识1、什么是限流?2、限流算法A) 静态算法a) 漏桶b) 令牌桶c) 固定窗口d) 滑动窗口B) 动态算法3、限流的模式4、 限流对象4、限流后应该怎么做?二、面试环节1、面试准备2、基本思路3、亮点展现A) 突发流量(针对请求个数而言)B) 请求大小(针对请求大小而言)…...

HDFS异构存储详解

异构存储 HDFS异构存储类型什么是异构存储异构存储类型如何让HDFS知道集群中的数据存储目录是那种类型存储介质 块存储选择策略选择策略说明选择策略的命令 案例&#xff1a;冷热温数据异构存储对应步骤 HDFS内存存储策略支持-- LAZY PERSIST介绍执行使用 HDFS异构存储类型 冷…...

《面试1v1》Kafka消息是采用Pull还是Push模式

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…...

Windows环境Docker安装

目录 安装Docker Desktop的步骤 Docker Desktop 更新WSL WSL 的手动安装步骤 Windows PowerShell 拉取&#xff08;Pull&#xff09;镜像 查看已下载的镜像 输出"Hello Docker!" Docker Desktop是Docker官方提供的用于Windows的图形化桌面应用程序&#xff0c…...

Spring 6.0官方文档示例(23): singleton类型的bean和prototype类型的bean协同工作的方法(二)

使用lookup-method: 一、实体类&#xff1a; package cn.edu.tju.domain2;import java.time.LocalDateTime; import java.util.Map;public class Command {private Map<String, Object> state;public Map<String, Object> getState() {return state;}public void …...

Docker Compose 容器编排

Docker compose Docker compose 实现单机容器集群编排管理&#xff08;使用一个模板文件定义多个应用容器的启动参数和依赖关系&#xff0c;并使用docker compose来根据这个模板文件的配置来启动容器&#xff09; 通俗来说就是把之前的多条docker run启动容器命令 转换为docker…...

while循环

while循环是一种常见的循环结构&#xff0c;它会重复执行一段代码&#xff0c;直到指定的条件不再满足。 基本语法如下&#xff1a; while 条件: # 循环体代码 其中&#xff0c;条件是一个布尔表达式&#xff0c;如果为True&#xff0c;则执行循环体中的代码&#xff1b;如果…...

从JVM指令看String对象的比较

在翻看各类 java 知识中&#xff0c;总会提到如下知识&#xff1a;比较 String 对象&#xff0c;例如&#xff1a; String a1new String("10"); String a2"10"; String a3"1""0";//结果 System.out.println(a1a2); //false System.ou…...

python与深度学习(六):CNN和手写数字识别二

目录 1. 说明2. 手写数字识别的CNN模型测试2.1 导入相关库2.2 加载数据和模型2.3 设置保存图片的路径2.4 加载图片2.5 图片预处理2.6 对图片进行预测2.7 显示图片 3. 完整代码和显示结果4. 多张图片进行测试的完整代码以及结果 1. 说明 本篇文章是对上篇文章训练的模型进行测试…...

Linux使用教程

一、Linux命令基础 1、ls、ll命令——展示数据 ①ls命令——平铺展示数据 其中ls命令以平铺的方式展现数据 ②ll命令——列表展示数据 ll命令以列表的方式展现数据 -a选项&#xff0c;表示&#xff1a;all的意思&#xff0c;即列出全部文件&#xff08;包含隐藏的文件/文件夹…...

项目名称:智能家居边缘网关项目

一&#xff0c;项目介绍 软件环境: C语言 硬件环境: STM32G030C8TX单片机开发板 开发工具: Linux平台GCC交叉编译环境以及ukeil (1)边缘网关概念 边缘网关是部署在网络边缘侧的网关&#xff0c;通过网络联接、协议转换等功能联接物理和数字世界&#xff0c;提供轻量化的联接管…...

SciencePub学术 | 物联网类重点SCIEEI征稿中

SciencePub学术 刊源推荐: 物联网类重点SCIE&EI征稿中&#xff01;信息如下&#xff0c;录满为止&#xff1a; 一、期刊概况&#xff1a; 物联网类重点SCIE&EI 【期刊简介】IF&#xff1a;7.5-8.0&#xff0c;JCR1区&#xff0c;中科院1/2区TOP&#xff1b; 【出版社…...

EtherNet/IP转Modbus网关以连接AB PLC

本案例为西门子S7-1200 PLC通过捷米特Modbus转EtherNet/IP网关捷米特JM-EIP-RTU连接AB PLC的配置案例。 网关分别从ETHERNET/IP一侧和MODBUS一侧读写数据&#xff0c;存入各自的缓冲区&#xff0c;网关内部将缓冲区的数据进行交换&#xff0c;从而实现两边数据的传输。 网关做为…...

mysql用户添加

一、连接mysql服务 mysql -u root -p 二、查询用户表 use mysql &#xff1b; SELECT User, Host FROM mysql.user; 三、新增用户并授权 Create USER dev4rw% IDENTIFIED WITH mysql_native_password BY 新密码; GRANT ALL PRIVILEGES ON *.* TO dev4rw% WITH GRANT OP…...

C++实现分布式网络通信框架RPC(3)--rpc调用端

目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中&#xff0c;我们已经大致实现了rpc服务端的各项功能代…...

【杂谈】-递归进化:人工智能的自我改进与监管挑战

递归进化&#xff1a;人工智能的自我改进与监管挑战 文章目录 递归进化&#xff1a;人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管&#xff1f;3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

ES6从入门到精通:前言

ES6简介 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript语言的重大更新&#xff0c;引入了许多新特性&#xff0c;包括语法糖、新数据类型、模块化支持等&#xff0c;显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var&#xf…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

分布式增量爬虫实现方案

之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面&#xff0c;避免重复抓取&#xff0c;以节省资源和时间。 在分布式环境下&#xff0c;增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路&#xff1a;将增量判…...

python报错No module named ‘tensorflow.keras‘

是由于不同版本的tensorflow下的keras所在的路径不同&#xff0c;结合所安装的tensorflow的目录结构修改from语句即可。 原语句&#xff1a; from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后&#xff1a; from tensorflow.python.keras.lay…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言&#xff1a; 最近在做行为检测相关的模型&#xff0c;用的是时空图卷积网络&#xff08;STGCN&#xff09;&#xff0c;但原有kinetic-400数据集数据质量较低&#xff0c;需要进行细粒度的标注&#xff0c;同时粗略搜了下已有开源工具基本都集中于图像分割这块&#xff0c…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用

文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么&#xff1f;1.1.2 感知机的工作原理 1.2 感知机的简单应用&#xff1a;基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...

AI语音助手的Python实现

引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...

系统掌握PyTorch:图解张量、Autograd、DataLoader、nn.Module与实战模型

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。 本文通过代码驱动的方式&#xff0c;系统讲解PyTorch核心概念和实战技巧&#xff0c;涵盖张量操作、自动微分、数据加载、模型构建和训练全流程&#…...