C++之多态篇(超详细版)

news2024/11/28 10:49:07

1.多态概念

多态就是多种形态,表示去完成某个行为时,当不同的人去完成时会有不同的形态,举个例子在车站买票,可以分为学生票,普通票,军人票,每种票的价格是不一样的,当你是不同的身份时去车站买票,就需要交不同的价钱,这个就是表示多态的行为。
在这里插入图片描述

2.多态的定义

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

上面的代码就是简单的多态定义,对于初学者看到上面的代码可能会一脸懵,别着急,容我细细为你们分析!
在这里插入图片描述

3.虚函数的重写

在上面我们提到了虚函数,解释了什么是虚函数,那么如何重写虚函数呢?

(1)重写虚函数(也叫覆盖)是派生类中重写出一个和基类的虚函数完全相同的虚函数,什么是完全相同呢?(派生类的虚函数和基类的虚函数的返回类型和函数名和参数列表都相同).

在这里插入图片描述
是不是派生类中虚函数也有virtual关键字,如果我们把它去掉可以吗?
我们看看下面代码

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

在这里插入图片描述
这里有老铁就会疑问了,为什么派生类虚函数可以没有virtual关键字呢?我们来调试一下代码吧
在这里插入图片描述
我们发现派生类继承下来了基类的虚函数,所以派生类也保持着虚函数的属性,所以程序没有问题,虽然程序没问题,但是这种写法不规范,不建议使用。

虚函数重写的两个特殊情况

1.协变:基类虚函数和派生类虚函数的返回值类型不同(基类返回的是基类对象的指针/引用;派生类返回的是派生类对象的指针/引用)

class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

2.虚构函数的重写(基类和派生类的函数名不同)

如果基类虚函数是析构函数,那么派生类的虚构函数无论有没有virtual关键字都会对基类析构函数构成重写。

class A {
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A 
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
};

int main()
{

	A* p1 = new A;
	B* p2 = new B;
	delete p1;
	delete p2;
	
	return 0;
}

在这里插入图片描述
代码完全没问题
如果派生类和基类析构函数的虚函数的函数名不同会也能构成重写。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

C++override和final关键字

看完上面的文章,我们知道C++的对函数的重写要求很严格,但在某些时候我们可能会出现写错函数名从而导致函数不能进行重载,这个错误编译阶段是不会报错的,所以如果我们debug就很难受了。所以C++11提供了override和final关键字来帮助我们检查是否完成重写。

我们来看看出现基类的虚函数和派生类的虚函数的函数名不同,看编译器会不会在编译阶段报错

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTickte()
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

在这里插入图片描述
编译阶段没有任何问题,我们再来看看运行结果,结果应该是买票全价和买票半价

###在这里插入图片描述结果出错了,在我们不知情的情况下去debug就很难查找出原因了。
我们再加上override关键字试试

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTickte() override
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

我们再来看看编译结果
在这里插入图片描述
直接就报错没有重写基类,所以证明了override关键字可以帮助我们检查派生类是否和基类构成重写。

我们明白了override关键字的作用,那么final关键字作用是什么呢?

final关键字修饰虚函数,表示该虚函数不能再被重写了。

class Person
{
public:
	virtual void BuyTicket() final
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket() 
	{
		cout << "买票半价" << endl;
	}
};

看看编译结果
在这里插入图片描述

重载/重写/重定义三个概念进行对比

在这里插入图片描述

4.抽象类

在虚函数后面写上=0,就表示纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象

//抽象类
class Person
{
public:x
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};
int main()
{
	Person Mike;
	return 0;
}

在这里插入图片描述
如果要实例化就直接报错了
那我们看看派生类继承了抽象类会怎么样?

//抽象类
class Person
{
public:
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void Ticket() 
	{
		cout << "买票半价" << endl;
	}
};
int main()
{
	Student s;
	return 0;
}

在这里插入图片描述
由此我们可知,就算我们派生类继承了抽象类也不能实例化出对象,只有重写基类的虚函数,派生类才能实例化出对象。

//抽象类
class Person
{
public:
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket() 
	{
		cout << "买票半价" << endl;
	}
};
int main()
{
	Student s;
	return 0;
}

在这里插入图片描述
代码没有任何问题。

接口继承和实例继承的区别

我们知道虚函数继承是接口继承,那什么是接口继承呢?接口继承是一个类从另一个类那里继承行为规范,但并不继承具体实现,接口继承就是一个契约,它规定了某个对象能做什么,但并没有规定要怎么做。
举个例子:假设你开了一个酒店,然后需要在酒店门口设置前台,为了确保前台能够为用户提供一致的服务体验,你创建了一个行为规范指南(这个就是接口)里面列出了前台服务员必须要给用户提供的服务体验。这个行为规范指南 就是一个接口,任何想要任职你酒店的前台就必须能够提供这些服务。

普通函数是一个实现继承,派生类继承的是基类的函数的实现。

5.多态的原理

看一下下面代码结果是什么。(Win32平台)

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

在这里插入图片描述
为什么是8字节呢?有老铁就疑惑了,不应该是4字节吗?那就和我一起来探索一下吧。
我们调试一下吧!
在这里插入图片描述
我们发现还有一个_vfptr指针,这个指针是干啥的呢?
这个_vfptr指针叫虚函数表指针,每一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址需要放到虚函数表中,虚函数表也叫虚表。

我们调试下面的代码看看

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
在这里插入图片描述
通过上面的代码,我们知道每一个虚函数都在虚函数表中存在一个指针,指向这个虚函数,普通函数在虚表中没有指向自己的指针;在虚表里面的指针可以分为两部分,一部分是从基类继承下来的虚函数,如果在派生类重写基类虚函数,就会把派生类对象的虚表里面的指针给覆盖掉,生成新的指针。,另一部分是派生类自己的虚函数。

我们通过调试窗口可以看到_vfptr虚表是不是一个存放指针的数组,一般这个数组后面都会以nullptr为结尾,

那么虚函数存放在哪呢?虚函数表又存放在哪里呢?
虚函数是和普通函数一样存放在代码段中,虚函数表中存放的是指向虚函数的指针,并不是虚函数本身,vs下的虚函数表是存放在代码段中。

我们来认证一下,看看vs编译器下虚函数表是不是存放在代码段中。

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

int main()
{
	Base s;
	printf("虚函数表的地址:%p\n", *(int*)&s);//只取前四个字节的地址

	static int a = 0;
	printf("静态区地址:%p\n", &a);

	const char* ch = "hello";
	printf("常量区:%p\n", ch);
}

在这里插入图片描述
这证明了虚函数表在常量区中

下面我们将通过画图来理解多态工作的原理
我们以下面的代码为例

class Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
	}
};
class Student : public Person 
{
public:
	virtual void BuyTicket() 
	{
		cout << "买票-半价" << endl; 
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

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

那么满足多态的函数调用是在编译阶段还是在运行阶段呢?
答案是运行阶段(但是虚表是在编译阶段就生成了),如果不满足多态的函数调用则是在编译阶段就调用对应的函数了。

动态绑定和静态绑定

动态绑定(后期绑定):在运行阶段,根据拿到的具体类型去确定程序的具体行为,调具体函数。
静态绑定:在编译阶段确定了程序行为(例如函数的重载)

单继承和多继承的虚函数表

我们知道派生类可以对基类进行单继承,也可以对基类进行多继承,那么两种继承方式的虚函数表有什么不同呢?

class Base 
{
public:
	virtual void func1() 
	{ cout << "Base::func1" << endl; }
	virtual void func2() 
	{ cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base 
{
public:
	virtual void func1() 
	{ cout << "Derive::func1" << endl; }
	virtual void func3() 
	{ cout << "Derive::func3" << endl; }
	virtual void func4() 
	{ cout << "Derive::func4" << endl; }
private:
	int b;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

我们来调试这段代码看看单继承的虚函数表
在这里插入图片描述
我们可以看到d对象继承了基类的虚函数,并重写了func1()函数,但是在d对象中应该还有func3和func4虚函数在虚表中,这里由于编译器隐藏起来了,所以我们看不到。

我们再来看看多继承的虚函数表

class Base1 
{
public:
	virtual void func1() 
	{ cout << "Base1::func1" << endl; }
	virtual void func2() 
	{ cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 
{
public:
	virtual void func1() 
	{ cout << "Base2::func1" << endl; }
	virtual void func2() 
	{ cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 
{
public:
	virtual void func1() 
	{ cout << "Derive::func1" << endl; }
	virtual void func3() 
	{ cout << "Derive::func3" << endl; }
private:
	int d1;
};

int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	return 0;
}

多继承的派生类的未重写的虚函数放在第一个继承基类部分虚函数表中
在这里插入图片描述

总结:

多态的概念比较晦涩难懂,希望各位老铁看完这篇文章能对多态有着清晰的理解!

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

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

相关文章

【JAVA开源】基于Vue和SpringBoot的旅游管理系统

本文项目编号 T 063 &#xff0c;文末自助获取源码 \color{red}{T063&#xff0c;文末自助获取源码} T063&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

【STM32开发之寄存器版】(二)-USART

一、前言 串口作为STM32的重要外设&#xff0c;对程序调试具有不可替代的作用。通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。其主要具备以下特性&am…

Nacos入门指南:服务发现与配置管理的全面解析

Nacos 是一个用于动态服务发现、配置管理和服务管理的平台。它由阿里巴巴开源&#xff0c;旨在帮助开发者更轻松地构建云原生应用。Nacos 支持多种环境下的服务管理和配置管理&#xff0c;包括但不限于 Kubernetes、Docker、虚拟机等。 一、Nacos的主要功能 1. **服务发现与健康…

GS-SLAM论文阅读笔记-CaRtGS

前言 这篇文章看起来有点像Photo-slam的续作&#xff0c;行文格式和图片类型很接近&#xff0c;而且貌似是出自同一所学校的&#xff0c;所以推测可能是Photo-slam的优化与改进方法&#xff0c;接下来具体看看改进了哪些地方。 文章目录 前言1.背景介绍GS-SLAM方法总结 2.关键…

认知杂谈97《兼听则明,偏听则暗》

内容摘要&#xff1a; 在信息爆炸的时代&#xff0c;我们被各种信息包围&#xff0c;这些信息往往经过精心设计以吸引注意力和影响观点。为了避免被操控&#xff0c;我们需要从多个渠道获取信息&#xff0c;并培养批判性思维来分析信息的真实性和偏见。 提高信息素养&#xff0…

读数据湖仓07描述性数据

1. 描述性数据 1.1. 基础数据中包含不同类型的数据&#xff0c;而不同类型数据的描述性数据也存在显著的差异 1.2. 尽管这些描述性数据存在根本性的差异&#xff0c;但通过描述性数据&#xff0c;我们可以全面了解基础数据中的数据 1.3. 通过分析基础设施中提供的描述性数据…

基于CAN总线的STM32G4 Bootloader设计说明

1 设计目的 根据芜湖铂科新能源自身企业发展需要&#xff0c;开发一款基于ST公司STM32G4系列MCU&#xff08;具体开发用型号STM32G473和STM32G431微处理器&#xff09;的CAN总线bootloader&#xff0c;方便应用程序的刷写。CAN设备采用周立功CAN卡&#xff08;USBCAN-II、CAN-…

Docker安装人大金仓(kingbase)关系型数据库教程

人大金仓数据库(KingbaseES)是由中国人民大学金仓公司研发的一款自主知识产权的关系型数据库管理系统。 官网地址:https://www.kingbase.com.cn/ 本章教程,主要介绍如何用Docker安装启动人大金仓(kingbase)关系型数据库。 一、下载镜像 下载地址:https://www.kingbase.c…

【黑马软件测试三】web功能测试、抓包

阶段三&#xff0c;内容看情况略过 Web功能测试链接测试表单测试搜索测试删除测试cookies/session测试数据库测试抓包工具的使用一个APP的完整测试流程熟悉APP业务流程功能测试APP专项测试兼容性安装、卸载和升级交叉测试(干扰测试)push消息测试用户体验测试 Web功能测试 通过…

Python画笔案例-075 绘制趣味正方形

1、绘制趣味正方形 通过 python 的turtle 库绘制 趣味正方形,如下图: 2、实现代码 绘制趣味正方形,以下为实现代码: """趣味正方形.py画个正方形后,单击它会移动,并且碰到边缘就反弹。这个版本采用画布的move命令让当前线条项目移动实现的。也可以用纯动画…

华夏ERP账号密码泄露漏洞

漏洞描述 华夏ERP账号密码泄露漏洞 漏洞复现 FOFA "jshERP-boot" POC IP/jshERP-boot/user/getAllList;.ico

解决 IntelliJ IDEA 中 JSP 页面无法识别 getParameter() 方法的问题

目录 背景: 过程: getParameter优点&#xff1a; 背景: 在IDEA中&#xff0c;我正在编写一个.jsp文件&#xff0c;想要测试一下数据是否能够从HTTP请求中成功获取到userId参数的数据&#xff0c;下面代码是我用来测试的&#xff0c;但是出现了错误。 <% String userId …

【EXCEL数据处理】000016案例 vlookup函数。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000016案例 vlookup函数。使用的软件&#xff1a;off…

SpringBoot整合QQ邮箱

SpringBoot可以通过导入依赖的方式集成多种技术&#xff0c;这当然少不了我们常用的邮箱&#xff0c;现在本章演示SpringBoot整合QQ邮箱发送邮件.... 下面按步骤进行&#xff1a; 1.获取QQ邮箱授权码 1.1 登录QQ邮箱 1.2 开启SMTP服务 找到下图中的SMTP服务区域&#xff0c;…

C/C++/EasyX——入门图形编程(4)

【说明】紧接上文(&#xff61;&#xff65;ω&#xff65;&#xff61;)&#xff0c;好了&#xff0c;接下来&#xff0c;就让我们开始学习图像处理和获取鼠标消息的函数吧。&#xff08;各位友友们不要着急&#xff0c;想在短时间内就想做小游戏或者写出各种好看的画面是不简…

【韩顺平Java笔记】第7章:面向对象编程(基础部分)【214-226】

文章目录 214. 递归解决什么问题215. 递归执行机制1216. 递归执行机制2217 递归执行机制3217.1 阶乘218. 递归执行机制4219. 斐波那契数列220. 猴子吃桃221. 222. 223. 224. 老鼠出迷宫1,2,3,4224.1 什么是回溯 225. 汉诺塔226. 八皇后 214. 递归解决什么问题 简单的说: 递归就…

Koa2+mongodb项目实战1(项目搭建)

Koa中文文档 Koa 是一个基于 Node.js 的 Web 应用框架&#xff0c;由 Express 原班人马打造。 Koa 并没有捆绑任何中间件&#xff0c;而是提供了一套优雅的方法&#xff0c;帮助开发者快速地编写服务端应用程序。 项目初始化 创建一个文件夹&#xff1a;ko2-mongodb 打开文件…

Nginx的基础讲解之重写conf文件

一、Nginx 1、什么是nginx&#xff1f; Nginx&#xff08;engine x&#xff09;是一个高性能的HTTP和反向代理web服务器&#xff0c;同时也提供了IMAP/POP3/SMTP服务。 2、用于什么场景 Nginx适用于各种规模的网站和应用程序&#xff0c;特别是需要高并发处理和负载均衡的场…

Python | Leetcode Python题解之第452题用最少数量的箭引爆气球

题目&#xff1a; 题解&#xff1a; class Solution:def findMinArrowShots(self, points: List[List[int]]) -> int:if not points:return 0points.sort(keylambda balloon: balloon[1])pos points[0][1]ans 1for balloon in points:if balloon[0] > pos:pos balloo…

【EO-1(Earth Observing-1)卫星】

EO-1&#xff08;Earth Observing-1&#xff09;卫星是美国国家航空航天局&#xff08;NASA&#xff09;新千年计划&#xff08;New Millennium Program&#xff0c;NMP&#xff09;地球探测部分中的第一颗对地观测卫星。以下是对EO-1卫星的详细介绍&#xff1a; 一、发射与服…