C++ ——多态 下 (图解多态原理、虚函数的再认知)

news2024/11/17 15:35:42

目录

一、抽象类

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;	// 定义一个指针  OK
	Shape& 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;    // 结果为 8
	D 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");
}

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

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

相关文章

【原创】java+swing+mysql教师管理系统设计与实现

教师管理系统主要是方便学校对教师进行管理&#xff0c;本文主要介绍如何使用java的swing窗体控件和mysql数据库去设计一个简单的教师管理系统。 功能分析&#xff1a; 本系统为javaswingmysql的教师管理系统&#xff0c;管理员、教师 功能如下&#xff1a; 管理员&#xff…

Quartz入门教程

本文参考文章编写 Quartz 官网 Quartz 是 OpenSymphony 开源组织在 Job Scheduling 领域又一个开源项目&#xff0c;是完全由 Java 开发的一个开源任务日程管理系统&#xff0c;“任务进度管理器”就是一个在预先确定&#xff08;被纳入日程&#xff09;的时间到达时&#xff…

2022——寒假总结

文章目录背景报名摸索结果总结背景 大一上学期&#xff0c;刚上大学没有尽快适应&#xff0c;什么都没有学到。 因为疫情&#xff0c;所以平时的测试以及期末都是线上进行的&#xff0c;就没怎么认真学&#xff0c;网课直接划水。 我的生活与学习很不平衡&#xff0c;还热衷于参…

搭建hadoop高可用集群(二)

搭建hadoop高可用集群&#xff08;一&#xff09;配置hadoophadoop-env.shworkerscore-site.xmlhdfs-site.xmlmapred-site.xmlyarn-site.xml/etc/profile拷贝集群首次启动1、先启动zk集群&#xff08;自动化脚本&#xff09;2、在hadoop151,hadoop152,hadoop153启动JournalNode…

【Kubernetes】【八】Namespace和Pod

第四章 实战入门 本章节将介绍如何在kubernetes集群中部署一个nginx服务&#xff0c;并且能够对其进行访问。 Namespace ​ Namespace是kubernetes系统中的一种非常重要资源&#xff0c;它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。 ​ 默认情况下&…

java ssm自习室选座预约系统开发springmvc

人工管理显然已无法应对时代的变化&#xff0c;而自习室选座预约系统开发能很好地解决这一问题&#xff0c;既能提高人力物力&#xff0c;又能提高预约选座的知名度&#xff0c;取代人工管理是必然趋势。 本自习室选座预约系统开发以SSM作为框架&#xff0c;JSP技术&#xff0c…

昇腾CANN算子开发揭秘

开发者在利用昇腾硬件进行神经网络模型训练或者推理的过程中&#xff0c;可能会遇到以下场景&#xff1a;1、训练场景下&#xff0c;将第三方框架&#xff08;例如TensorFlow、PyTorch等&#xff09;的网络训练脚本迁移到昇腾AI处理器时遇到了不支持的算子。2、推理场景下&…

buu [WUSTCTF2020]大数计算 1

题目描述&#xff1a; 题目分析&#xff1a; part1:直接用 阶乘计算器&#xff0c;得出答案38609695part2:python代码&#xff0c;得出答案&#xff1a;67358675 print(520**13142333**666)part3:直接搜索宇宙终极问题&#xff0c;得到以下知识&#xff1a; 题目就是要我们给…

德赛西威NAV75*-SV731*导航升级(凯立德J30)实战

一、前言&#xff1a;升级导航德赛西威&#xff08;2015年买的&#xff09;地图几年没升级过了&#xff08;之前自己折腾了一个&#xff09;之前的启动是DSA2013&#xff08;电子G已经无法升级数据文件了&#xff0c;本次只升级地图J30图资-凯立德&#xff09;主程序版本&#…

超实用的小红书内容营销策略分享!纯干货

抓住小红书内容流量密码就是掌握了财富&#xff0c;越来越多的品牌方和商家都在小红书上收获了相当可观的用户流量&#xff0c;如果你的小红书营销没有什么起色&#xff0c;那绝对是没有走对方向。 小红书是一个内容为王的平台&#xff0c;如果你还不懂下面这些小红书内容营销…

VSCode设置eslint自动缩进和自动格式化代码

文章目录VSCode设置eslint自动缩进和自动格式化代码1、找到VS Code的settings.json文件2、修改settings.json文件内容为如下内容3、打开项目根文件夹下的 .eslintrc.js文件4、在rules 下配置“缩进几个空格”5、配置完保存&#xff0c;结束配置。VSCode设置eslint自动缩进和自动…

Linux——操作系统安装

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。个人爱好: 编程&#xff0c;打篮球&#xff0c;计算机知识个人名言&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石…

初识自动化测试工具——katalon

一、什么是Katalon Katalon Studio 是一款免费的自动化测试工具&#xff0c;可以安装在windows、macOS、linux操作系统上&#xff0c;基于selenium 和 Appium 测试框架&#xff0c;并集成了这些框架的优点。工具使用简单方便&#xff0c;对于编码经验少&#xff0c;能力弱的测…

mysql调优-内存缓冲池

因本地查询和服务器查询相比服务器慢了很多&#xff0c;同样的数据&#xff0c;同样的sql查询&#xff0c;考虑了是不是链接太多了&#xff0c;自行查询了下&#xff0c;我使用的c3p0的链接池&#xff0c;配置一个小时超时&#xff0c;正常情况下是20多个链接&#xff0c;而mys…

linux下监测串口数据

在编写上下位机通信代码时&#xff0c;需要分阶段测试&#xff0c;确保下位机&#xff0c;线路&#xff0c;上位机都&#xff2f;&#xff2b;&#xff0e; 一&#xff0e;检查设备数据传出 &#xff11;&#xff0e;确定下位机的串口参数 如果波特率有问题&#xff0c;可能会…

SVN转GIT

SVN迁移至GitPS&#xff1a;进入正文前&#xff0c;提一句题外话&#xff0c;建议参考官网教程看&#xff0c;因为很多情况&#xff0c;别人写的只是针对自己所对应的场景&#xff0c;可能并不符合你所面对的场景&#xff0c;这里附上官网教程链接&#xff1a;Git官网迁移教程收…

【JavaWeb】传输层协议——UDP + TCP

目录 UDP协议 UDP协议结构 UDP的特点 TCP协议 TCP协议结构 TCP的特点 TCP的十个核心机制 确认应答 超时重传 连接管理 滑动窗口 流量控制 阻塞控制 延迟应答 捎带应答 粘包问题 异常处理 UDP协议 UDP协议结构 源端口&#xff1a;存储的是发送方的端口号。 目的…

Python循环语句代码详解:while、for、break

目录 1 while循环 1 while循环 循环语句是程序设计中常用的语句之一。任何编程语言都有while循环&#xff0c;Python也不例外。while循环的格式如下所示。 while(表达式): … else: … while循环的执行过程&#xff1a;当循环表达式为真时&#xff0c;依次执行whi…

使用webpack(4版本)搭建vue2项目

在学习webpack之前&#xff0c;也从网上搜过一些用webpack搭建vue项目的博客&#xff0c;但是在自己使用的时候会报各种的问题&#xff0c;报错的根本原因其实就是版本的问题&#xff0c;以下代码是经过解决了许多报错问题研究出来最简单最方便搭建vue2项目的方法首先创建一个空…

配置okta saml验证单点登录splunk

目标 使用okta作为splunk单点登录身份提供程序&#xff0c;通过saml身份验证配置&#xff0c;可实现通过okta平台账号单点登录splunk应用 环境准备 1. okta环境 首先在okta上注册一个账号&#xff0c;注册地址https://login.okta.com/signin/register/ &#xff0c;注册完成…