C++ ——多态 下 (图解多态原理、虚函数的再认知)
目录
一、抽象类
1)抽象类定义
2)抽象类的继承
3)抽象类实现多态
4)抽象类的好处
二、多态的实现原理
1)虚函数的存储方式
2)子类中虚函数的存储方式
① 子类将基类中的虚表原封不动的拷贝到自己的虚表中
② 如果子类中重写了基类中的虚函数,则将子类虚表中基类的虚函数替换为子类自己重写了的虚函数地址
③ 如果子类有新增的虚函数,则接着将子类新增的虚函数按照其在子类中的声明顺序依次增加到子类虚表的尾部
3)代码检验虚函数在虚表中的先后位置
4)对于多态的原理的剖析
① 当不满足多态的条件时
② 满足多态的条件(通过指针或者引用调用)
三、其他继承方式下的虚表模型
1)单继承方式
2)多继承方式
源码:(注意调用TestPrintB1与TestPrintB2方法获取类中虚表的顺序)
一、抽象类
在多态上那篇文章中,写了这样一个类Shape,然后通过子类圆、矩形、三角形分别继承该Shape类基类,通过重写基类中的Print与GetPerimeter这俩个虚函数然后达到了多态的效果。
可是这里仍然有一些问题,就是这个基类的函数Print与GetPerimeter这俩个函数的执行对于基类对象来说,用Shape定义一个对象s,这个s是个啥呢?它是一个图形?它是个啥图形?不免得引发争议,这个Shape就不应该定义对象,它的功能函数也不应该被实现!!!
class Shape
{
public:virtual void Print(){cout << "未知图形" << endl;}virtual double GetPerimeter(){cout << "未知图形,无法获得周长" << endl;}
};
这个Shape就是一个抽象类,形状这个概念在问题领域中不是直接存在的,它只是一个抽象概念。
1)抽象类定义
在虚函数的后面写上=0,表示该函数为纯虚函数,包含纯虚函数的类称为抽象类(也叫接口类),抽象类不能实例化出对象。
class Shape
{
public:virtual void Print() = 0;virtual double GetPerimeter() = 0;
};
2)抽象类的继承
抽象类可以被继承,但子类中如果没有对基类所有的纯虚函数进行重写,则子类也无法进行实例化对象。(即子类也是一个抽象类)
对基类中的所有纯虚函数进行重写之后才能完成子类实例化对象:
class Shape
{
public:virtual void Print() = 0; // 限定为纯虚函数,表示在子类中必须要对其进行重写virtual int GetPerimeter() = 0;
};class Triangle : public Shape
{
public:Triangle(int a, int b, int c): _a(a), _b(b), _c(c){}virtual void Print()override // 养成给基类中需要重写的虚函数加上override的习惯{cout << "△" << endl;}virtual int GetPerimeter()override{return _a + _b + _c;}
private:int _a;int _b;int _c;
};
int main()
{Triangle t(3, 4, 5); // 正常编译
}
3)抽象类实现多态
上面说了抽象类无法实例化对象,但是抽象类也是一种类型,所以它可以创建指针或者引用。
那么就可以用到多态来传递参数
void Test(Shape& s) // 传入Shape类型的引用
{s.Print();cout << s.GetPerimeter() << endl;
}int main()
{Triangle t(3, 4, 5);Test(t); // 实现多态Shape s; // 实例化对象报错 !!!Shape* ptrs = &t; // 定义一个指针 OKShape& rs = t; // 定义一个引用 OK // 基类的指针或引用可以指向子类return 0;
}
4)抽象类的好处
① 代码更加符合逻辑——有些类就是无法创建对象,所以将它声明为抽象类
② 不用花费时间来考虑纯虚函数中的代码应该怎么写
③ 抽象类实际规范了:后续子类要实现的虚函数的原型 ==》将接口规范化
二、多态的实现原理
这里使用的是VS2019 x86 32位的编译环境下进行研究(一个指针为4个字节)
1)虚函数的存储方式
class D
{
public:D(int d, int b): _d(d){cout << "B::B()" << endl;}virtual void func1(){cout << "D::func1()" << endl;}virtual void func2(){cout << "D::func2()" << endl;}virtual void func3(){cout << "D::func1()" << endl;}int _d;
};int main()
{cout << sizeof(D) << endl; // 结果为 8D d(1,2);return 0;
}
创建一个D类,类中存在三个虚函数,这三个虚函数在类中的模型是如何呢?
① 调试并查看用D创建出来的对象d:发现d中前四个字节存放的是一个类似于地址的东西,接着存放的是_d的值也占用四个字节,所以sizeof(D)为8个字节
② 那么这四个字节的地址是什么呢?
展开这个地址发现里面又存放了三个地址,而这三个地址都指向了我定义的三个虚函数!
③ 调用内存窗口看一下&d,然后再打开另外一个窗口查看一下d中前四个字节的地址存放的是啥
④ 对比这内存窗口与监视窗口的这三个地址,发现就是②中所说的,这块地址指向了三个虚函数的地址。
⑤ 模型推导
⑥ 同一个类中定义俩个对象,它们的虚表地址是相同的。(共用同一张虚表)
结论:
如果一个类中存在虚函数(类的大小多个4个字节)
① 则编译时编译器会为类中的虚函数创建一个虚表(编译器一定会生成类的构造方法)
② 虚表中的内容存放的是各个虚函数的入口地址,
③ 并且虚表中虚函数的先后次序和类中定义的虚函数的先后次序一致。
④ 同一个类中的多个对象共用同一张虚表
2)子类中虚函数的存储方式
上面了解了关于类中虚函数是如何存储的,那么下面通过继承来学习子类中的虚函数的存储方式
① 子类将基类中的虚表原封不动的拷贝到自己的虚表中
如图将B中的俩个虚函数继承下来后置入自己的虚表中。函数访问时通过虚表指针来进行访问。
class B{
public:int _b;virtual void func1(){cout << "B::func1()" << endl;}virtual void func2(){cout << "B::func2()" << endl;}
};class D : public B{
public:int _d;
};
② 如果子类中重写了基类中的虚函数,则将子类虚表中基类的虚函数替换为子类自己重写了的虚函数地址
D子类重写了基类的虚函数func1():
③ 如果子类有新增的虚函数,则接着将子类新增的虚函数按照其在子类中的声明顺序依次增加到子类虚表的尾部
通过内存监视窗口了解虚表内的函数存储(D::func3()为在子类D中新增的虚函数,D::func1为子类重写了基类的虚函数,B::func2为基类继承下来的虚函数)
class B
{
public:int _b;virtual void func1(){cout << "B::func1()" << endl;}virtual void func2(){cout << "B::func2()" << endl;}
};class D : public B
{
public:int _d;virtual void func1(){cout << "D::func1()" << endl;}virtual void func3(){cout << "D::func3()" << endl;}
};
3)代码检验虚函数在虚表中的先后位置
通过上面的学习可以知道,通过一个对象的内存模型可以知道其前四个字节为虚表指针,那么通过这个虚表指针就可以访问到存储虚函数入口地址的函数指针数组,接着通过这个函数指针数组来访问每个函数的入口地址
① 获取虚表指针内容
使用一个四个字节大小的int类型的指针来进行强转d对象,则这个接收结果的p现在就指向了0xcd9b64这个地址,如果这时候对这个p解引用则就可以得到虚表指针的内容
② 对p进行解引用
解引用后的值指向了虚表指针数组(可是这里的data是一个int类型的整形数据——下面看到0xcd9b64是我通过设计将整数16进制转化看到的)
③ 对data整形数据进行类型转换
首先看一下函数指针如何取别名,对void(*)()取别名为
typede void(*PVft)();
PVft* fp = (PVft*) data;
④ 从上到下依次打印虚表中的内容
while (*fp){(*fp)();fp++;}
4)对于多态的原理的剖析
① 当不满足多态的条件时
例如下面使用基类的对象来直接进行调用函数
编译器在编译阶段就可以确定调用哪个类的函数,因为此时编译器编译时看到的是对象的静态类型,是哪个类的对象就去哪个类里面调用它的成员函数。
② 满足多态的条件(通过指针或者引用调用)
直白的,当了解了子类虚函数的存储结构后,多态其实也就是一种特殊的函数调用机制,它通过传入的对象指针判断访问的是哪个对象的类型,然后通过该类型的虚表确定该如何来调用入口函数
三、其他继承方式下的虚表模型
1)单继承方式
单继承方式下的虚表模型就是上面我们一直所探讨的用例
父类与子类中各有一张虚表(当然是在有虚函数的前提下)
2)多继承方式
如以下继承体系:B1(func1、func2)、B2(func3、func4)为俩个基类,D继承自这俩个基类,D中重写了B1中的func1、B2中的func4、以及新增虚函数func5
在多继承中,子类中存在俩个虚表,子类新增的虚函数地址按次序放到第一张虚表后面
源码:(注意调用TestPrintB1与TestPrintB2方法获取类中虚表的顺序)
class B1
{
public:int _b1;virtual void func1(){cout << "B1::func1()" << endl;}virtual void func2(){cout << "B1::func2()" << endl;}
};class B2
{
public:int _b2;virtual void func3(){cout << "B2::func3()" << endl;}virtual void func4(){cout << "B2::func4()" << endl;}
};
class D : public B1, public B2
{
public:int _d;virtual void func1()// 重写父类B1的虚函数{cout << "D::func1()" << endl;}virtual void func4()// 重写父类B2的虚函数{cout << "D::func4()" << endl;}virtual void func5()// 子类新增虚函数{cout << "D::func5()" << endl;}
};typedef void(*VFPT)();
void TestPrintB1(B1& b, const string& info)
{cout << info << endl;VFPT* fp = (VFPT*)*(int*)&b;while (*fp){(*fp)();fp++;}cout << "======================" << endl;
}
void TestPrintB2(B2& b, const string& info)
{cout << info << endl;VFPT* fp = (VFPT*)*(int*)&b;while (*fp){(*fp)();fp++;}cout << "======================" << endl;
}int main()
{D d;d._b1 = 1;d._b2 = 2;d._d = 3;TestPrintB1(d, "D of B1");TestPrintB2(d, "D of B1");
}
相关文章:

C++ ——多态 下 (图解多态原理、虚函数的再认知)
目录 一、抽象类 1)抽象类定义 2)抽象类的继承 3)抽象类实现多态 4)抽象类的好处 二、多态的实现原理 1)虚函数的存储方式 2)子类中虚函数的存储方式 ① 子类将基类中的虚表原封不动的拷贝到自己的…...

cocos creater 3.x 构建QQ小游戏
一、目前 cocos creater 不支持直接构建QQ小游戏,需要构建成微信小游戏,然后修改成QQ小游戏 二、构建QQ小游戏不能勾选 分离引擎 的选项,勾选分离引擎的选项,需要安装cocos微信小游戏引擎插件,这个插件似乎目前只支持微…...

ArcGIS笔记3_如何编辑、修改和导出散点数据
本文目录前言Step 1 在ArcGIS中添加并显示坐标点Step 2 将坐标数据保存成shp文件Step 3 编辑或修改坐标数据Step 4 导出修改后的数据:法一:通过转换工具导出Step 5 导出修改后的数据:法二:通过dBASE表导出前言 本博文更多针对Arc…...

Computer Graphics From Scratch - Chapter 8
系列文章目录 简介:Computer Graphics From Scratch-《从零开始的计算机图形学》简介 第一章: Computer Graphics From Scratch - Chapter 1 介绍性概念 第二章:Computer Graphics From Scratch - Chapter 2 基本光线追踪 第三章:Computer Gr…...
金三银四”不香了?
“金三银四”不香了? “金三银四”这个词,放在三年前,勾勒的是无数踌躇满志的年轻人涌向职场,大中小企业血液更新与流动的鲜活画面。 尤其是互联网行业,这个在过去20多年里极大改变文化交流方式与商业形态的领域&…...

个人开源PCB开发板列表汇总
个人开源PCB开发板列表汇总✨首先感谢立创EDA的免费打样和立创一起开源的广大网页。 🔰STC单片机为主控开源PCB开发板列表 📌STC15F2K60S2开发板:https://oshwhub.com/perseverance51/stc15f2k60s2-ji-tong-ban 📌STC15W408AS系…...

2023美国大学生数学建模竞赛(美赛)思路代码
2023美国大学生数学建模竞赛(美赛)思路&代码报名时间节点比赛说明问题A(数据分析题):收干旱影响的植物群落(MCM)第一问第二问问题B(仿真建模题):重塑马赛…...

makefile简易教程
makefile简易教程 一、学习目标 达到多文件快速编译的需求,相关符号的意思,以及其它注意事项。 二、快速入门 2.1 基本概念 Makefile 是一个在Unix和Linux操作系统上使用的构建工具,用于自动化编译和构建源代码。 2.2 用处 通过Makefi…...

快速入门nginx
目录 1.nginx前言 2.什么是nginx 3.Nginx作用? 1.正向代理 2.反向代理 3.轮询 4.加权轮询 4.Nginx的安装 1.windows下安装 2.linux下安装 5.Nginx常用命令 1.nginx前言 我们公司项目刚刚上线的时候,并发量小,用户使用的少&#…...

甘特图:项目管理工具,轻松简化工作流程
项目规模越大,管理就越复杂,有时候甚至一个项目经理需要管理多个项目,当多个项目、多条任务同时进行,项目所涉及的范围广,内容越来越复杂,使得项目越难以把控,好的管理工具,可以提升…...

刷题专练之翻转题练习
文章目录一、 编写函数实现字符串翻转二、轮转数组总结一、 编写函数实现字符串翻转 描述 编写一个函数,实现字符串的翻转 输入描述: 输入一个字符串 输出描述: 输出翻转后的字符串 写法一: 这种方法是定义begin和end࿰…...
【Java】死锁
一、什么是死锁 死锁指多个线程在执行过程中,因争夺资源造成的一种相互等待的僵局。 进程死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。…...
DS图—图的最短路径(无框架)迪杰斯特拉算法
目录 题目描述 AC代码 题目描述 给出一个图的邻接矩阵,输入顶点v,用迪杰斯特拉算法求顶点v到其它顶点的最短路径。 输入 第一行输入t,表示有t个测试实例 第二行输入顶点数n和n个顶点信息 第三行起,每行输入邻接矩阵的一行&…...

【笔记】数据异常检测与修复总结
文章目录一、异常种类1. 对于移动对象的数据异常2. 对于时序数据的异常检测二、异常数据清洗流程三、数据预处理四、异常检测算法五、异常修复算法六、漂移数据清洗一、异常种类 不同的研究对象,有着不同的异常分类方式 1. 对于移动对象的数据异常 异常数据信息&…...

算法笔记(七)—— 图的相关知识及算法
图的存储方式 1. 邻接表(记录关于某点的直接相邻点) 2. 邻接矩阵(一定是正方形的矩阵,对点进行编号,点到点的权值由距震中的值表示,无直接相连记为正无穷) 图的模板 unordered_map<int,No…...
ssh配置互信时错误解决方法
之前项目中遇到有关配置ssh互信免密登录问题,为避免以后踩坑,现记录一下避坑指南。 1、提示如下错误: Permission denied (publickey,gssapi-keyex,gssapi-with-mic). 问题分析:可能是ssh配置问题。 查看日志/var/log/secure&…...
SQL69 返回产品并且按照价格排序
描述有Products 表prod_idprod_nameprod_pricea0011egg3a0019sockets4b0019coffee15【问题】编写 SQL 语句,返回 Products 表中所有价格在 3 美元到 6 美元之间的产品的名称(prod_name)和价格(prod_price),…...

vue+elementUI 实现设置还款日字母弹窗组件
1、业务背景 还款业务,设置每月还款日,选每月几号扣款,不需要29、30、31,因为不是每个月都有这三天的 2、预期效果图 3、代码实现 3.1 初始化vue项目 地址:https://cn.vuejs.org/guide/introduction.html 3.2 在项…...

【JavaGuide面试总结】Redis篇·中
【JavaGuide面试总结】Redis篇中1.Redis 单线程模型了解吗?2.Redis6.0 之后为何引入了多线程?3.Redis 是如何判断数据是否过期的呢?4.过期的数据的删除策略了解么?5.Redis 内存淘汰机制了解么?6.什么是 RDB 持久化&…...
Python:每日一题之全球变暖(BFS连通性判断)
题目描述 你有一张某海域 NxN 像素的照片,"."表示海洋、"#"表示陆地,如下所示: ....... .##.... .##.... ....##. ..####. ...###. ....... 其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...

Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...