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 像素的照片,"."表示海洋、"#"表示陆地,如下所示: ....... .##.... .##.... ....##. ..####. ...###. ....... 其中"上下左右"四个方向上连在一起的一片陆地组成一座岛屿…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
Vue 模板语句的数据来源
🧩 Vue 模板语句的数据来源:全方位解析 Vue 模板(<template> 部分)中的表达式、指令绑定(如 v-bind, v-on)和插值({{ }})都在一个特定的作用域内求值。这个作用域由当前 组件…...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...
