C/C++(六)多态

news2024/10/24 23:44:46

本文将介绍C++的另一个基于继承的重要且复杂的机制,多态

一、多态的概念

多态,就是多种形态,通俗来说就是不同的对象去完成某个行为,会产生不同的状态

多态严格意义上分为静态多态动态多态我们平常说的多态一般指动态多态。后文介绍的多态也是动态多态,只在本部分介绍一下静态多态)

1、静态多态

静态多态又称作静态绑定(早绑定、前期绑定),即在函数编译期间就决定了程序的行为(即函数名修饰规则,具体C/C++(二)中有详细描述)。

平常最经常用的静态多态就是函数重载

2、动态多态

动态多态又称作后期绑定,在程序运行期间再根据具体拿到的类型来调用具体的函数,确认程序的具体行为。

我们平常说的多态一般指动态多态,静态动态一般就说函数重载。

重载(静态多态)、虚函数重写(动态多态)、隐藏的区别

二、多态(动态多态) 

从技术方面来说,多态就是不同继承关系下的类对象,去调用同一函数(调用的函数必须是虚函数,后文会介绍),会产生不同行为

1、多态的构成条件

1、调用的函数必须是虚函数,且派生类必须为基类的虚函数进行重写。

2、必须用父类的指针 / 引用来调用虚函数。

(为什么必须传父类的指针 / 引用?这里初步解释,后面会在原理部分详细解释——因为父子类的赋值兼容原则,子类可以切片赋值给父类,父类却不能赋值给子类,因为可能会缺成员)

(那又为什么必须传指针 / 引用?因为传对象的话,子类只会把父类的那一部分成员拷贝过去,但是不会拷贝虚函数表指针,就不能成功调用对应的虚函数了)

2、虚函数

被 virtual 修饰的类成员函数称为虚函数

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

2.1  虚函数的重写(多态的条件之一)

如果派生类中存在与父类完全相同(函数名、函数返回值、函数参数都完全相同)虚函数,就称作派生类的虚函数重写了父类的虚函数

#include <iostream>
using namespace std;

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

class Student :public Person
{
public:
	/*
		子类重写父类虚函数时,如果不加 virtual 关键字,虽然也可以构成重写(子类继承下来父类的虚函数,仍旧保持虚函数属性)
		但是这种写法不规范,可读性较差,建议不要这么做
	*/
	virtual void BuyTicket()
	{
		cout << "半价购票" << endl;
	}
};

void Test(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Test(p);
	Test(s);
	return 0;
}
运行结果可以发现,传父子类,分别调用父子类的虚函数

2.2  多态的两个特殊情况

2.1.1  协变(基类与派生类的虚函数返回值类型不同的时候)

当派生类重写父类虚函数的时候,基类与派生类的虚函数的返回值类型可以不同,但是必须是父类 / 子类的指针或引用

当派生类虚函数返回值是父类 / 子类的指针或引用时,称作协变

2.2.2  析构函数的重写

如果基类的析构函数也是虚函数,这个时候只要派生类定义了析构函数,不论是否加了 virtual 关键字,都视作对基类的析构函数构成重写

(虽然基类和派生类的析构函数名字不同,看似违背了虚函数的重写原则,实际上编译器会对析构函数的名称做特殊处理,在编译后,所有析构函数的名称都会统一处理成 destructor)

#include <iostream>
using namespace std;

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

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

// 只有派生类Student的析构函数也定义了析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}
父类调用析构函数,子类调用析构函数,先调用里面的子类析构,再调用父类析构

3、C++11检测虚函数是否重写的两个关键字

从上文的介绍可以看出,C++对虚函数的重写要求比较严格。在有些情况下(比如函数名、返回值字母写反写错),可能会无法构成重写,导致无法构成多态。

但是这种错误在编译期间是不会报出的,只有在程序运行时才会发现,与预期结果不符,这个时候才来debug,得不偿失。

因此C++11标准提供了两个帮助用户检测是否完成重写的关键字:final  和  override

3.1  final

final 修饰某个虚函数,则这个虚函数不能再被重写

3.2  override

override 修饰派生类虚函数,检查派生类的虚函数是否基类的某个虚函数的重写,如果不是(比如拼写错了),编译报错。

4、纯虚函数与抽象类

在虚函数的后面加上 =0 ,这样的虚函数称作纯虚函数

包含纯虚函数的类叫做抽象类(又叫接口类,在某类不代表具体实体的时候可以使用;另一个意义是说明多态想在其多个子类中实现)抽象类不能实例化出对象

继承抽象类的派生类也不能实例化出对象,只有当这个派生类对纯虚函数进行重写,这个派生类才能实例化出对象。

因此纯虚函数在某种程度上间接强制了派生类的重写,更体现了接口继承思想。

(接口继承与实现继承:

普通函数的继承是一种实现继承,继承的是函数的实现,目的是使用这个函数

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写达成多态。

三、多态的实现原理(重点) 

1、代码引入

#include <iostream>
using namespace std;

class Test
{
public:
	virtual void test()
	{
		cout << _num << endl;
	}
private:
	int _num = 1;
};

int main()
{
    Test t;
	printf("%d", sizeof(Test));
}

让我们猜猜,sizeof(Test) 应该是多少?

很多人可能会说,函数储存在代码段里,不算在类大小里面,那就应该是4字节(32位系统) / 8字节(64位系统)

但实际上:

x86环境下

 

x64环境下

这是为什么?

通过内存窗口的观察我们可以看见,Test对象里面除了储存了_num 成员变量,还储存了一个叫做_vfptr的指针变量,而一切指针变量大小在32位系统下都是4字节,在64位系统下都是8字节。

这个_vfptr是什么?这个指针我们叫做虚函数表指针,指向虚函数表。(v代表virtual,f 代表function)

 2、虚函数表

虚函数表的本质,是储存着一个类里面的所有虚函数地址的一个指针数组一般情况下这个数组最后会放一个nullptr作为虚函数表的终止标记。)(注意:不是储存着虚函数,是储存着虚函数的地址,虚函数还是储存在代码段里的)

我们给出一个多态的代码:

#include <iostream>
using namespace std;


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;
};

// 派生类Derive继承Base并重写Func1
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

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

调用一下监视窗口:

我们可以发现:

1、派生类对象 d 由两部分构成,继承自父类的成员,和自己的成员

2、派生类和父类都有一个虚函数表指针,指向各自的虚函数表,虚函数表里面储存着虚函数的地址。

3、派生类的虚函数表和父类的虚函数表不一样,由于Func1完成了重写,所以d的虚表

中存的是重写的Derive::Func1;派生类完成重写了的虚函数覆盖了原有的父类虚函数。

所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

4、派生类其实把父类的三个函数都继承了下来,但是由于Func3不是虚函数,所以并未放到虚函数表中。

派生类虚函数表的生成流程:

1、先把基类的虚函数表拷贝到自己的虚函数表中

2、如果派生类重写了某个虚函数,在虚函数表中用这个虚函数地址覆盖原父类的虚函数地址3、派生类如果自己增加了虚函数,按照在派生类中的声明次序依次放到派生类虚函数表的后3、多态的原理

3、多态的实现原理

还是直接上代码: 

#include <iostream>
using namespace std;

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;
}
多态实现概念图
多态实现代码图

观察多态实现代码图:

观察红色箭头可以看到,p在指向mike对象时,p->BuyTicket从mike的虚表中找到的虚

函数是Person::BuyTicket。

观察蓝色箭头可以看到,p在指向johnson对象时,p->BuyTicket在johson的虚表中

找到的虚函数是Student::BuyTicket。

这样就实现出了不同类的对象去调用同一函数时,展现出不同的形态

再看一下汇编代码:

// 与多态无关的汇编代码都已去除
void Func(Person* p)
{
...
 p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE  mov         eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1  mov         edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE  mov         eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA  call        eax  
00头1940EC  cmp         esi,esp  
}
int main()
{
... 
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调
用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
 mike.BuyTicket();
00195182  lea         ecx,[mike]
00195185  call        Person::BuyTicket (01914F6h)  
... 
}

就可以明白,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的,因此叫做动态多态。

4、多态是如何实现的?(一句话总结)

首先,多态是一种基于继承和虚函数实现的机制,用来调用虚函数的函数必须传父类的指针或引用;然后,基类和派生类各有一张虚函数表,通过传参的不同(父类直接传,子类切片),对象内部的虚函数表指针会去各自的虚函数表里面寻找对应的虚函数地址,从而实现调用同名函数时产生不同的行为,达到多态的效果。

5、有关多态的一些小问题:

如果子类不重写虚函数,父子类的虚函数表一样吗?

储存的虚函数的地址是一样的,但是虚函数表毕竟是两张表,储存虚函数表的地方不一样,是分开存储的!

如果有许多同类对象,它们的虚函数表一样吗?

一样!同类对象共用一张虚函数表!

也就是说,虚函数表本质其实是个静态常量,被所有同类对象共享!

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

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

相关文章

VulkanTutorial(1·环境搭建,渲染流程简述)

介绍&#xff1a; 与OpenGL&#xff0c;WebGL和Direct3D等API&#xff08;(Application Programming Interface, 应用程序编程接口)&#xff09;相比&#xff0c;valkan更偏向于底层&#xff0c;有更多的GPU控制接口&#xff0c;因此它有更好的性能和更小的驱动开销&#xff0…

【Python数据可视化】利用Matplotlib绘制美丽图表!

【Python数据可视化】利用Matplotlib绘制美丽图表&#xff01; 数据可视化是数据分析过程中的重要步骤&#xff0c;它能直观地展示数据的趋势、分布和相关性&#xff0c;帮助我们做出明智的决策。在 Python 中&#xff0c;Matplotlib 是最常用的可视化库之一&#xff0c;它功能…

【论文+源码】基于spring boot的垃圾分类网站

创建一个基于Spring Boot的垃圾分类网站涉及多个步骤&#xff0c;包括环境搭建、项目创建、数据库设计、后端服务开发、前端页面设计等。下面我将引导您完成这个过程。 第一步&#xff1a;准备环境 确保您的开发环境中安装了以下工具&#xff1a; Java JDK 8 或更高版本Mav…

python装饰器的另类用法

在对pyverilog源码进行单步调试时&#xff0c;遇到一个很奇怪的现象&#xff0c;被装饰器装饰的方法t_LINECOMMENT没有主动调用&#xff0c;但装饰器TOKEN中的内嵌函数set_regex却被调用了。 ## lexer.pyfrom ply.lex import *class VerilogLexer(object):linecomment r"…

C++【string类的使用】(上)

文章目录 1. 为什么要学习string类2. 标准库的string类2.1 string的构造函数&#xff08;1&#xff09;无参构造&#xff08;重点&#xff09;&#xff08;2&#xff09;用字符串初始化&#xff08;重点&#xff09;&#xff08;3&#xff09;用字符串的前n个字符初始化(4)拷贝…

常见ElasticSearch 面试题解析(上)

前言 ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java语言开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是一种流行的企业级搜索引擎。ElasticSearch…

直播间“饕餮盛宴”的背后,是“他经济”正在冒头

最近&#xff0c;一个有意思的现象逐渐露出了苗头。 今年“双11”第一轮尾款开启支付的当晚&#xff0c;罗永浩的直播间上演了一出别样的“饕餮盛宴”。直播开场后&#xff0c;iphone16系列、可口可乐&#xff08;含糖、无糖300ml&#xff09;10秒售罄&#xff0c;索尼PS5、沃…

hive on tez 指定队列后任务一直处于running状态

如上图所示一直处于running状态&#xff0c;查看日志发现一直重复弹出同一个info&#xff1a; 2024-10-18 16:57:32,739 [INFO] [AMRM Callback Handler Thread] |rm.YarnTaskSchedulerService|: Allocated: <memory:0, vCores:0> 释义: 当前应用程序没有分配到任何内存…

wordcloud 字体报错

wordcloud 字体报错 词云库报错&#xff1a;Only supported for TrueType fonts字体文件问题pillow版本的问题wordcloud版本问题&#xff08;我的最终解决方案&#xff09; 词云库报错&#xff1a;Only supported for TrueType fonts 字体文件问题 解决方法 写绝对路径 &…

教程分享!超简单的低功耗4G模组LCD应用示例!看过来~

低功耗4G模组LCD应用是物联网技术中的一部分知识&#xff0c;在未来的学习和实践中&#xff0c;我们还将接触到更多前沿的技术和理念。让我们一起努力&#xff0c;探索科技的无限可能&#xff0c;为我们的生活带来更多便利与惊喜&#xff01;希望本文能为您提供一些帮助&#x…

Cisco WLC 9800 - HA SSO with Ether-channel

本文将记录如何配置HA SSO以及Ethernet Channel。 1.拓扑情况 本文的内容基于如下的Topo进行。 2.准备工作 两台WLC的型号必须一样&#xff1b;两台WLC必须使用一样的软件版本&#xff1b;需要准备好使用的IP地址&#xff1a;两个用于WLC的管理地址&#xff08;WMI&#xff…

串口通讯编程示例之串口编写程序

使用open()函数打开串口设备 首先使用open()函数打开串口设备/dev/ttymxc6&#xff0c;设备使用了O_RDWR | O_NOCTTY | O_NDELAY标志&#xff0c;分别代表以读写方式打开、不让设备成为控制终端且设置非阻塞模式&#xff0c;也就是当无法打开设备时&#xff0c;不会在原地等待&…

【博客节选】Unity角色异常抖动问题排查

本文截取自本人文章 &#xff1a;【Unity实战笔记】第二一 基于状态模式的角色控制——以UnityChan为例 发现出现角色抖动问题 尝试解决方法&#xff1a; 跳跃的loop time不要勾选&#xff1b; 相机aim添加垂直阻尼 还是不行&#xff0c;仔细查看是位移时震颤。 UnityCha…

HCIP-HarmonyOS Application Developer 习题(十三)

&#xff08;多选&#xff09;1、在设计应用框架的过程中&#xff0c;我们常用的界面应用框架有哪些? A、启动页 B、详情页 C、列表视图 D、网格视图 答案&#xff1a;ABCD 分析&#xff1a; &#xff08;多选&#xff09;2、触摸屏以触控的方式进行输入。它可以支持以下哪些…

OpenIPC开源FPV之Ardupilot配置

OpenIPC开源FPV之Ardupilot配置 1. 源由2. 问题3. 分析3.1 MAVLINK_MSG_ID_RAW_IMU3.2 MAVLINK_MSG_ID_SYS_STATUS3.3 MAVLINK_MSG_ID_BATTERY_STATUS3.4 MAVLINK_MSG_ID_RC_CHANNELS_RAW3.5 MAVLINK_MSG_ID_GPS_RAW_INT3.6 MAVLINK_MSG_ID_VFR_HUD3.7 MAVLINK_MSG_ID_GLOBAL_P…

千万不要小看SD3.5!最强模型全家桶来了!

一、SD3.5 的登场 Stability AI 推出的 SD3.5 引起了广泛关注。它直接开源了三个模型&#xff0c;包括 Large 和 Large Turbo&#xff0c;Medium 将于 29 号发布&#xff0c;并且这三个型号都可以商用。 &#xff08;一&#xff09;模型版本介绍 模型版本参数量特点分辨率范围S…

《A complete telomere-to-telomere assembly of the maize genome》方法总结

研究背景 完整的T2T基因组组装一直是基因组研究的长期追求。 研究方法 通过生成高深度覆盖的超长 Oxford Nanopore Technology (ONT) 和 PacBio HiFi 测序数据&#xff0c;报道了玉米的完整基因组组装。 每条染色体均以单一contig的形式完整覆盖。 结果概述 基因组特征&am…

vue3中mitt和pinia的区别和主要用途,是否有可重合的部分?

在 Vue 中&#xff0c;Mitt 和 Pinia 是两个不同的工具&#xff0c;它们的主要用途和功能有所不同&#xff0c;但在某些方面也存在重合的部分。 区别 Mitt&#xff1a; Mitt 是一个简单而强大的事件总线库&#xff0c;用于在组件之间进行事件的发布和订阅。 它提供了一种简洁…

【Linux】实现一个简易的shell命令行

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:Linux ⚙️操作环境:Xshell (操作系统:Ubuntu 22.04 server 64bit) 目录 一.项目简介 二.分析项目实现 三.逐步实现项目功能 1.获取命令行 2.解析命令行 3.指令的判断 4.普通命令的执行 四.完整项目代码 结语 一.项目简介…

计算生物学与生物信息学漫谈-1-测序一路走来

最近工作中&#xff0c;反思自己计算生物学基础非常薄弱&#xff0c;然而作为一门非常新兴的交叉学科&#xff0c;涉及计算机、物理、生物、数学等多多学科&#xff0c;国内并没有这样完善的教程&#xff0c;因此想要自己做一个教程&#xff0c;使用费曼学习法学习&#xff0c;…