【C++】多态(下)

ops/2025/2/12 14:05:02/

大家好,我是苏貝,本篇博客带大家了解C++的多态,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
在这里插入图片描述


目录

  • 4. 多态的原理
    • 4.1 虚函数表
    • 4.2 多态的原理
    • 4.3 动态绑定与静态绑定
  • 5. 单继承和多继承关系的虚函数表
    • 5.1 单继承中的虚函数表
    • 5.2 多继承中的虚函数表
    • 5.3 菱形继承/菱形虚拟继承(了解)

4. 多态的原理

4.1 虚函数表

问:下面代码的结果是什么?
在这里插入图片描述

答案是8,为什么?类Base里只有1个int类型的变量,它占4个字节,结果是8,那说明还有4个字节是因为虚函数存在的,是什么?看下图
在这里插入图片描述

通过观察测试我们发现b对象中除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分
在这里插入图片描述

虚函数表里存放的是虚函数的地址

我们再来多一些虚函数
在这里插入图片描述
在这里插入图片描述

通过观察和测试,我们发现了以下几点问题:
1、 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,父类的虚表指针也继承了,另一部分是自己的成员
2、 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3、 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
4、 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

总结一下派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

上面说的都是不同类的对象的虚函数表,那如果是同类的对象,它们的虚函数表是什么情况呢?
在这里插入图片描述

同类的对象共用一个虚函数表

4.2 多态的原理

上面分析这个半天了,那么多态的原理到底是什么?还记得下面的Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket
在这里插入图片描述
在这里插入图片描述

1、 观察上图的红色箭头我们看到,p是指向pt对象时,p->BuyTicket在pt的虚表中找到的虚函数是Person::BuyTicket。
2、 观察上图的蓝色箭头我们看到,p是指向st对象时,p->BuyTicket在st的虚表中找到的虚函数是Student::BuyTicket。
3、 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

满足多态以后的函数调用需要的函数地址,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时需要的函数地址是编译时确认好的。

现在我们来打印一下对象d的虚表
在这里插入图片描述

根据上面的学习,我们知道,虚表是一个函数指针数组,
在这里插入图片描述

但是按照上图的函数指针数组的格式就太麻烦了,因此我们typedef一下
在这里插入图片描述

再写一个打印虚表的函数
在这里插入图片描述

最后只需要将虚表指针传进Print函数,即需要将d的第一个4字节的内容(虚表指针)作为参数
我们可以将d强转成int类型的吗?不能,因为只有相关联的类型才能相互转换

在这里插入图片描述

所以我们先&d,再取出前4个字节的地址,即(int*)&d,再得到前4个字节地址的内容,即*((int*)&d),最后在将它强转成VFPTR*即可
在这里插入图片描述

在这里插入图片描述

我们还想知道这3个函数地址分别对应哪个函数怎么办?调用对应函数

在这里插入图片描述

在这里插入图片描述

这里还有一个很容易混淆的问题:虚函数存在于哪个区域? 虚表存在于哪个区域?
答:虚表存的是虚函数指针,不是虚函数,虚函数和普通成员函数一样的,都是存在于代码段(即常量区)的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下也是存在于代码段的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过结果我们发现,A和B的虚表地址离位于常量区的”hhhhh”更近,所以大致能证明虚表存在于常量区(即代码段)

4.3 动态绑定与静态绑定

1、 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
2、 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

5. 单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中的虚函数表,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

5.1 单继承中的虚函数表

在这里插入图片描述

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?

在这里插入图片描述

1、 打开内存窗口
在这里插入图片描述

2、 使用代码打印出虚表中的函数。(在11.4的第2点多态的原理的最后有详细介绍)

在这里插入图片描述
在这里插入图片描述

结论:
如果子类有虚函数,继承的父类有虚函数表指针,那就将子类的虚函数地址放到第一个有虚函数表指针的父类的虚函数表中。
如果继承的父类没有虚函数表指针,那就子类自己创建一个虚函数表并存储虚表的地址

5.2 多继承中的虚函数表

在这里插入图片描述

问:上面程序的答案是什么?
在这里插入图片描述

答案:20,为什么?

在这里插入图片描述

Derive本身没有虚表指针吗?没有,因为它继承的基类有虚函数表,所以Derive将func3的函数地址放到了第一个虚表指针所指向的虚表中

下面通过打印虚函数表来证明
在这里插入图片描述
在这里插入图片描述

如果出现上图的情况,那就先清理解决方案,再重新生成解决方案
在这里插入图片描述

再运行程序就正常了
在这里插入图片描述

观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

5.3 菱形继承/菱形虚拟继承(了解)

1、菱形继承:
在这里插入图片描述

在这里插入图片描述

菱形继承的对象模型和多继承相似,如果派生类有不是继承的虚函数A,如果继承的基类有虚表指针,那么将虚函数A放到第一个有虚表指针的基类的虚表中。

在这里插入图片描述

2、菱形虚拟继承
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

虚基表:在菱形继承那部分有讲到


好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️


http://www.ppmy.cn/ops/157136.html

相关文章

STM32 软件I2C读写MPU6050

接线图 代码配置 软件I2C只需要用GPIO的读写函数就行,在软件I2C初始化需要把SCL和SDA都初始化为开漏输出模式,还需要把SCL和SDA置高电平 1.配置初始化函数 //MyI2C初始化函数 void MyI2C_Init(void) {//配置GPIOGPIO_InitTypeDef GPIO_InitStructure;…

mac下生成.icns图标

笔记原因: 今日需要在mac下开发涉及图标文件的使用及icons文件的生成,所以记录一下。 网络上都是一堆命令行需要打印太麻烦了,写一个一键脚本。 步骤一 将需要生成的png格式文件重命名为“pic.png” mv xxxx.png pic.png 步骤二 下载我…

实在RPA案例|视源股份:驱动20+核心场景数字化升级,组织效能提升超80%

广州视源电子科技股份有限公司(以下简称 “视源股份”,股票代码:002841.SZ)是广东省大型上市企业,旗下产品常年占据全国份额第一,成功孵出 “液晶电视主控板卡、希沃(seewo)教育交互…

Vue的Diff算法与React的Diff算法有何不同?

Vue 的 Diff 算法与 React 的 Diff 算法的区别 在前端开发中,Diff 算法是虚拟 DOM 的核心,负责比较新旧虚拟 DOM 的差异,以便高效地更新真实 DOM。虽然 Vue 和 React 都使用虚拟 DOM 技术,但它们的 Diff 算法在实现和策略上有显著的不同。本文将详细探讨这两者的主要区别。…

通过STM32实现外设控制应用案例

1.GPIO控制LED闪烁 功能描述:使用STM32的GPIO引脚控制LED的亮灭,实现LED的闪烁效果。 代码示例(基于HAL库): #include “main.h”#include “stm32f4xx_hal.h” void SystemClock_Config(void);static void MX_GPIO_Init(void); int main(void){HAL_Init(); SystemClock_…

【Android】Android开发应用如何开启任务栏消息通知

Android开发应用如何开启任务栏消息通知 1. 获取通知权限2.编写通知工具类3. 进行任务栏消息通知 1. 获取通知权限 在 AndroidManifest.xml 里加上权限配置&#xff0c;如下。 <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android…

保姆级教程Docker部署KRaft模式的Kafka官方镜像

目录 一、安装Docker及可视化工具 二、单节点部署 1、创建挂载目录 2、运行Kafka容器 3、Compose运行Kafka容器 4、查看Kafka运行状态 三、集群部署 四、部署可视化工具 1、创建挂载目录 2、运行Kafka-ui容器 3、Compose运行Kafka-ui容器 4、查看Kafka-ui运行状态 …

集合类不安全问题

ArrayList不是线程安全类&#xff0c;在多线程同时写的情况下&#xff0c;会抛出java.util.ConcurrentModificationException异常 解决办法&#xff1a; 1.使用Vector&#xff08;ArrayList所有方法加synchronized&#xff0c;太重&#xff09; 2.使用Collections.synchronized…