【C++】-----多态及原理

news2024/11/6 9:37:05

目录

前言

一、是什么?

二、怎么样?

Ⅰ、构成条件

Ⅱ、虚函数 

Ⅲ、虚函数的重写

1.常规情况下

2.虚函数重写的三个例外

 ①返回值的类型可以不同

②析构函数的重写 

 ③子类虚函数可以不加virtual关键字(不建议)

3.override和final关键字

 Ⅳ、重载/重写/重定义(隐藏)区别

三、虚函数表(细致)

场景引入

定义

单继承的虚表

如何打印虚表(指针)?

多继承的虚表

菱形虚拟继承的虚表

总结:

四、底层原理

原理

多态调用与普通调用

五、抽象类

定义

特点


前言

都说面向对象的三大特性为封装,继承,多态,前面已经介绍了封装和继承,下面来看看C++多态是怎么个事。。

一、是什么?

多态,多态,通俗点来说就是一个事物有多种形态,具体点就是对于同一件事情,不同对象去完成时会产生不同的形态,结果不一样!

比如:买车票这件事,有普通票,有学生票半价,军人就优先买票等等

二、怎么样?

多态是在不同继承关系的类对象,去调用同一函数,从而产生了不同的行为!!

多态的特点:指向谁就调用谁!

Ⅰ、构成条件

  • 子类完成对父类虚函数的重写
  • 通过父类的指针或者引用去调用虚函数

因此实现多态,指向谁,就调用谁!!

Ⅱ、虚函数 

所谓虚函数,就是在成员函数前面加上virtual关键字 。

注意:

①是成员函数,不是函数!!!不能在类外声明和定义

②静态成员函数不能设置为虚函数,因为没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

③inline成员函数可以是虚函数。编译器会忽略inline属性。

 如:

virtual void Buytick()

Ⅲ、虚函数的重写

1.常规情况下

重写也称为覆盖,即子类中有一个和父类完全一样(函数名,参数列表,返回值三者)的成员函数,称为子类重写了父类的虚函数。。

可以看到,没有完成虚函数重写,不构成多态,结果就不是多态的结果!

 以上常规情况下必须完成三同的条件,但是你知道的,这是C++,必然会有例外,也叫坑!

2.虚函数重写的三个例外

 ①返回值的类型可以不同

这种情况也叫做协变,类型不同,但是仅限父类虚函数返回父类的指针/引用,子类虚函数返回子类的指针/引用。

 

②析构函数的重写 
如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然函数名不同。看似违背重写规则,其实不然,因为 实际上编译器统一将析构函数的名称处理为destructor。这样函数名实际就是相同的。构成重写。

有以下场景:

可以看到上面的结果,如果子类中申请了空间,但是没有释放,必然会引起内存的泄漏 ,所以为了防止上述场景的发生,我们希望指向谁就调用谁的析构,那就要构成多态。所以建议在继承体系中,将析构函数定义为虚函数。。构成多态。。

 ③子类虚函数可以不加virtual关键字(不建议)

 

对于这种例外,不建议这样干,建议最好全加上。

注意:构成多态,父类不能不加!!

 为什么可以不加呢?

重写更深一层的含义:子类继承父类,实际上将虚函数头继承了,也就是说虚函数头实际还是和父类完全一样子类只是完成功能上的重写,也就是把这个虚函数头的函数体换了。所以可以不加。

3.override和final关键字

  •  final,修饰虚函数,表示该虚函数不能继承;修饰类时,表示该类不能被继承。加父类

  •  override,检查子类虚函数是否重写父类的某个虚函数,没有重写就报错。加子类

 Ⅳ、重载/重写/重定义(隐藏)区别

  • 重载

①两个函数在同一作用域

②函数名相同,参数不同

  • 重写

①两个函数必须是虚函数

②两个函数分别在父类和子类

③两个函数满足三同(函数名,参数,返回值),协变例外

  • 重定义(隐藏)

①两个函数分别在父类和子类中

②两函数名相同即可

三、虚函数表(细致)

场景引入

问题:以下代码类对象的大小是多大?

class Base
{
public:
	virtual void Func() 
	{
		cout << "Is Base::Func()" << endl;
	}
private:
	int _a=1;
};

Base b;
sizeof(b)=?
	

 按照正常理解,类对象中存的是类成员变量,所以大小应该是4,但是事实并非如此!

可以看到结果是8,为啥呢?调试看看

除了有成员变量以外,还有一个_vfptr,这什么玩意啊?实际上就是一个指针,也叫做虚函数表指针。在32位环境下,一个指针的大小就是4字节,所以上面的结果就是 int + 指针大小=8!

定义

虚函数表就是存放虚函数地址的一个表。实际上,对于一个有虚函数的类至少都有一个虚函数表指针,该指针指向一个虚函数表,虚函数表简称虚表,虚函数表指针简称虚表指针!

注意区分:

虚表里面存的不是虚函数,而是虚函数的地址(指针)!!!只有虚函数才能进入这个表!

②虚表的本质就是一个存虚函数指针的指针数组,一般这个数组最后一个放nullptr!

③虚表是在编译阶段就生成的,一般在常量区!

虚表指针存在于实例化的对象上!

⑤虚函数跟普通函数一样存在代码段,不在虚表!

继承部分提到的虚基表,存的当前位置距离虚基类部分的偏移量,解决菱形继承数据冗余和二义性

单继承的虚表

上图我们说过,只有一个函数是虚函数时,才能将其地址放入虚表中,那来看看下面的例子

class Base
{
public:
	virtual void Func1()
	{
		cout << "Is Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
};
class Devire:public Base
{
public:
	virtual void Func1()
	{
		cout << "Is Devire::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Is Devire::Func3()" << endl;
	}
};

Base b;
Devire d;

实际上这个可以理解为编译器的一个BUG。。将Func3给隐藏了,不正常现象!那Fun3究竟在哪?只能打印虚函数表来看看了。

如何打印虚表(指针)?

虚表实际上是一个函数指针数组,元素是函数指针,也就是每一个元素都为void(*)()类型。所以为了方便我们可以重命名。

typedef void(*VFR)();将函数指针类型重命名为VFR

VFR p[10];函数指针数组。元素VFR

虚表的访问需要使用对象中虚表指针才能进行访问,所以得取出对象的虚表指针(数组指针),指向虚表。32位环境下,一个指针的大小为4字节,所以就要取出对象的头4字节。怎么取?直接强制类型转化不可以!

①取出d对象地址,强制类型转化成int*,在解引用,此时就拿到一个整型的大小,4个字节。这个值就是指向虚表的指针。。

②在强转成VFR*,因为虚表就是一个存VFR类型(函数指针类型)的数组。

VFR* ptr = (VFR*)(*(int*)&d);//取出虚表指针。

打印函数:

//void func(VFR ptr[])
void func(VFR* ptr)
{
	cout << "虚表地址->>" << ptr << endl;
	for (size_t i = 0; i<3; i++)
	{
		printf("第%d虚函数的地址:%p->",i, ptr[i]);
		VFR f = ptr[i];//取出数组的每一个元素,类型为函数指针。

		(*f)();//指针解引用,函数调用
	}
}

验证一下结果: 

可以看到Func3也在对象d中的虚表中。。。只是被编译器隐藏了!!!BUG

所以实际上上图,单继承子类的对象模型是:

多继承的虚表

实际上,多继承体系中,子类没有重写的虚函数(自己的虚函数),会被放在第一个继承基类部分的虚函数表中!

分析如下:

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int _a;
};
class B
{
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func2() { cout << "B::func2" << endl; }
private:
	int _b;
};
class C : public B, public A
{
public:
	virtual void func1() { cout << "C::func1" << endl; }
	virtual void func3() { cout << "C::func2" << endl; }
private:
	int _c;
};

C c

上图的监视窗口可以看到,子类C重写了B和C的func1,那么C自己的虚函数Func3在哪?是否可能会在两个父类的虚表中的一个?打印虚表看看。

从上图可以很清楚的看出,C自己的虚函数放在了父类B的虚表中!!

注意:找到父类A的虚表指针,更加保险的写法:

A* ptr = &c;
VFR* vTableb2 = (VFR*)(*(int*)ptr);
func(vTableb2);

所以实际上,C的对象模型是这样的:

菱形虚拟继承的虚表

有如下继承关系:

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
private:
	int _a=1;
};
class B:virtual public A
{
public:
	virtual void func2() { cout << "B::func2" << endl; }
private:
	int _b=2;
};
class C :virtual public A
{
public:
	virtual void func3() { cout << "C::func3" << endl; }
private:
	int _c=3;
};
class D :public B, public C
{
public:
	virtual void func4() { cout << "D::func4" << endl; }
private:
	int _d=4;
};

D d
sizeof(d)=?

 

 

对于菱形虚拟继承,实际上存在三张虚表,A、B、C都有虚表。因为虚基类A是共享的,B和C的虚函数不可能同时放入A的虚表中,所以B和C存在各自的虚表,另外又是虚拟继承,所以还存在着一个虚基表指针,虚基表中存的偏移值,为了去寻找父类A,方便切片赋值!

 

总结:

  • 父类没有虚表或者是共享类的时候,子类都需要单独创建虚表存自己的虚函数。如上A是B、C的共享类,B、C就要创建虚表
  • 父类存在虚表,子类对象就不需要单独创建了,直接放继承下来的

不要写菱形虚拟继承,太复杂了!! 

四、底层原理

原理

 结论:若程序满足多态条件运行时,就会去指向对象的虚函数表中找到对应的虚函数地址,再根据虚函数地址调用对应的虚函数。指向父类调用父类的虚函数,指向子类调用子类的虚函数!

分析如下:

买票代码:

class Person
{
public:
	virtual void Buytick() 
	{
		cout << "普通-全价" << endl;
	}
};
class Stu :public Person
{
public:
	virtual void Buytick()
	{
		cout << "学生-半价" << endl;
	}
};
void Func(Person& p)
{
	p.Buytick();
}
int main()
{
	Person p;
	Stu s;
	return 0;
}

来看看子类Stu的虚表是怎样的。

①子类会将父类的虚表内容拷贝一份放到自己的虚表中,如果子类重写了父类中的某个虚函数,那就会用自己重写后的虚函数去覆盖父类的虚函数(如上Buytick)。子类自己新增的虚函数依次按声明顺序放入虚表。

②当你将子类对象赋值给父类指针或者引用时,切出去的父类虚函数已经被子类的覆盖了。

综上可得:父类和子类的虚函数表是完全不一样的两张表同类型的对象共用一张虚表,不同类型的对象有各自的虚表。正因如此,才能实现指向谁就调用谁

多态调用与普通调用

多态调用:满足多态条件以后的函数调用,不是在编译时确定的,而是程序运行起来以后到对象中找的。因为对象是在运行时才有的,而虚表指针又存在对象中。。

运行起来才确定所调用的函数行为,也称为动态绑定(晚期绑定)

②普通调用:不满足多态的函数调用,在编译时就确定了。根据调用对象的类型,确定调用函数。

在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载

五、抽象类

定义

在C++中的抽象类和Java不一样,C++的抽象类:在虚函数的后面写上 =0,该函数为纯虚函数!包含这种纯虚函数的类叫做抽象类

//Car为抽象类
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};

特点

抽象类不能实例化出对象

②若子类没有重写纯虚函数,该子类也不能实例化出对象,重写后才可以实例化!但父类依旧不能实例化出对象

间接强制派生类重写虚函数

和override的区别就是:override是检查语法是否有问题,能实例化对象!抽象类不能实例化出对象!

 看需求,若不希望父类实例化出对象,那就搞成抽象类!

①普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
②虚函数的继承是一种接口继承 ,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。
③如果不实现多态,不要把函数定义成虚函数。

 好了,今天的分享就到这里,如果对你有帮助,欢迎三连,你的支持和认可就是我前进的动力!

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

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

相关文章

极米RS10 Plus VS当贝X5S Pro!最强客厅投影仪选当贝投影才是正解

不知道为什么&#xff0c;2024年的投影仪市场迭代迅猛&#xff0c;尤其是很多头部品牌小升级不断&#xff0c;机型后缀错综复杂让消费者更难下定决心做出抉择。而在今天又有一款新品极米RS10 Plus正式发布&#xff0c;同价位其实早前就有热门人气选手当贝X5S Pro坐镇&#xff0…

ChatGPT的封号和停止注册应对和常见问题丨出海笔记

ChatGPT "亚洲区封号"和“停止注册”的事情大家都听说过吧&#xff0c;我认为&#xff0c;官方有一定程度的“控频”&#xff0c;但并没有一杆子打死&#xff0c;更没有自媒体吹嘘的所谓封号和无法注册&#xff0c;因为我曾给朋友注册了2个以及升级plus成功&#xff…

MM 12 -采购- 成本中心采购

思维导图 说明 采购申请 手工或BPM接口创建 物料组&#xff1a; 必输 科目分配类别K &#xff0c;标签页会增加 科目分配 标签页 会计科目&#xff1a; 根据物料组带出 或者直接输入&#xff0c;根据情景。 成本中心需要填写。 采购订单 科目分配类别K &#xff0c;标签页会增…

【Windows】EFI系统盘重新安装操作系统遇到磁盘MBR分区解决办法

【Windows】EFI系统盘重新安装操作系统遇到磁盘MBR分区解决办法 1.背景2.问题3.解决 1.背景 本博客使用ventoy软件制作USB闪存启动盘。 相关博客&#xff1a; 【windows10】ventoy软件制作USB闪存启动盘-CentOS8 https://blog.csdn.net/jn10010537/article/details/123283985…

C到C++——C++基础

C是一种通用的、静态类型的、跨平台的编程语言。它是在1979年由Bjarne Stroustrup创建的&#xff0c;最初是作为C语言的扩展来支持面向对象编程。 C在保留C语言的特性的同时&#xff0c;添加了许多其他的功能&#xff0c;包括类、对象、继承、多态、模板等。这使得C成为了一种…

“金牌挑战——奥运知识大比拼”微信小程序线上知识竞赛答题活动复盘总结

一、活动背景 奥运会进行得如火如荼&#xff0c;为了弘扬奥运精神&#xff0c;激发公众对于奥林匹克运动的兴趣和热情&#xff0c;我们特别策划了“金牌挑战——奥运知识大比拼”线上知识竞赛活动。本次活动依托微信小程序&#xff0c;通过趣味性和互动性强的知识竞答&#xf…

SOPHGO算能科技BM1684盒子占用空间满的问题解决

目录 1 问题由来 2 问题排查与解决 1 问题由来 安装软件的时候发现&#xff0c;软件根本安装不上了&#xff0c;用df -h看到根目录已经满了 rootbm1684:~# df -h Filesystem Size Used Avail Use% Mounted on overlay 5.8G 5.7G 0 100% / devtmpfs …

【实战】MFC客户端Python后端之仿造QQ聊天

项目概述 这里介绍一个很多年以前做的一个小项目&#xff0c;新手小白可以参考学习。本项目旨在开发一个功能丰富的即时通讯及聊天室系统&#xff0c;类似于QQ&#xff0c;具备客户端与服务端通讯、多人聊天室、界面友好度、一对一聊天、通讯内容加密、服务端与数据库交互等功能…

十八.核心动画 - 使用CAGradientLayer图层构建渐变视图

引言 在现代的UI设计中&#xff0c;渐变色和圆角已经成为了不可或缺的元素。无论是应用程序的背景&#xff0c;按钮&#xff0c;还是图标&#xff0c;这些设计趋势不仅使界面更加美观&#xff0c;还能提升用户体验。特别是渐变色&#xff0c;它通过颜色的平滑过渡&#xff0c;…

如何利用绩效考核来强化员工对TPM的参与度?

TPM&#xff08;Total Productive Maintenance, 全面生产维护&#xff09;作为一种追求生产系统效率最大化的管理模式&#xff0c;其核心在于通过全员参与和持续改进&#xff0c;实现设备综合效率的最大化。然而&#xff0c;要让这一理念深入人心&#xff0c;并转化为员工的日常…

图片转为pdf怎么弄?亲测有效的8个pdf转换方法安利

图片转PDF怎么弄&#xff1f;在日常的办公生活中&#xff0c;我们经常会需要处理一些文档格式转换难题&#xff0c;图片转成PDF格式就是其中一个&#xff0c;图片转换成PDF格式的话&#xff0c;方便我们传输分享&#xff0c;毕竟现在PDF格式凭借着自身的稳定性和可移植性已经成…

李晨晨的嵌入式学习 DAY20

今天主要对zuot学习函数进行了补充 一&#xff0c;文件IO函数 1.fileno函数 类型转换函数 函数原型&#xff1a;int fileno(FILE *stream); 功能&#xff1a;fileno函数用于取得参数stream指定的文件流所使用的文件描述符。文件描述符是一个非负整数&#xff0c;用于在底层…

C/C++开发,opencv光流法跟踪特征点

目录 一、Lucas-Kanade光流法 1.1cv::ORB特征点提取方法 1.2 cv::calcOpticalFlowPyrLK函数 二、完整案例实现 2.1 程序代码 2.2 程序编译及输出 2.3 读取视频文件方式补充 一、Lucas-Kanade光流法 在 OpenCV 中&#xff0c;使用 特征检测器(例如ORB ,Oriented FAST and…

基于深度学习的地磁活动、扰动预测模型

注&#xff1a;包括SYM-H Index和Storm Intensity index A transformer-based framework for predicting geomagnetic indices with uncertainty quantification Journal of Intelligent Information Systems 18 November 2023 A transformer-based framework for predicting…

IP地址怎样实现安全的HTTPS访问?

IP实现HTTPS访问是一个涉及证书申请、服务器配置及网络安全的过程。以下是实现IP实现HTTPS访问的详细步骤&#xff1a; 公网IP地址的重要性&#xff1a;要实现HTTPS访问&#xff0c;必须拥有一个公网IP地址&#xff0c;这是从互联网直接访问网站的基础条件。 管理权限的必要性&…

高效批量提取PPT幻灯片中图片的方法

处理包含大量图片的PPT&#xff08;PowerPoint&#xff09;幻灯片已成为许多专业人士的日常任务之一。然而&#xff0c;手动从每张幻灯片中逐一提取图片不仅耗时耗力&#xff0c;还容易出错。为了提升工作效率&#xff0c;减少重复劳动&#xff0c;探索并实现一种高效批量提取P…

“网络信息安全”你真的了解吗?(非常详细)零基础入门到精通,收藏这一篇就够了

全面了解网络信息安全 01 导语&#xff1a; 在数字化浪潮中&#xff0c;我们每个人的生活都越来越依赖于网络。银行账户、个人隐私、企业机密——几乎所有的敏感信息都在网络上流转。随之而来的是不断升级的网络攻击和诈骗手段。本文将深入探讨网络信息安全的意义、挑战、防…

Candance Allegro 入门教程笔记:Cadence Allegro 17.4安装教程

文章目录 一、安装Cadence Allegro 17.4 安装包二、安装Candance Allegro Manager三、安装007号 补丁四、用阿狸狗破戒大师 破戒Candance Allegro 17.4软件 Cadence Allegro QQ交流学习裙&#xff1a;173416628 凡亿教育的Candance Allegro 17.4基础教程 小哥Cadence Allegro …

SSM伊犁旅游攻略网站—计算机毕业设计源码15961

目 录 摘要 1 绪论 1.1 开发背景 1.2开发意义 1.3ssm框架 1.4论文结构与章节安排 2 2 伊犁旅游攻略网站系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1功能性分析 2.3.2非功能性分析…

48天笔试训练错题——day43

目录 选择题 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 编程题 1. 求和 选择题 1. synflood 是 syn 泛洪攻击。有一个恶意主机&#xff0c;伪造大量的 IP 地址&#xff0c;然后给服务器发送 SYN 请求&#xff0c;但是不进行第三次握手的回复&#xff0c;这样就会消耗服务器…