C++面向对象三大特性之---多态

news2024/12/23 10:59:28

一、多态的概念

        多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。比如同样是买票的操作,学生买票就会打折,而普通的成人买票就是全款。

二、多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2虚函数

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

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

2.3虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

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

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

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

注意:派生类重写虚函数可以不加virtual关键字,因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性(基类一定要加virtual才能构成多态,否则就是隐藏了)

虚函数重写的两个例外:

1.协变(基类与派生类虚函数返回值类型不同)

基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用

2. 析构函数的重写(基类与派生类析构函数的名字不同)

首先我们先看一个现象:

        我们让两个Person类型的指针分别指向了Person对象和Student对象,然后delete这两个指针,delete除了会free掉指针外,还会调用类的析构函数,由于我们分别new了一个Person对象和一个Student对象,所以我们期望析构的时候应该一个调用Person的析构函数另一个调用Student的析构函数,但是结果我们发现调用的都是Person的析构函数,这样就会出现很大的问题,例如:如果Student类中存在资源申请的话就会造成内存泄露。

        造成上述结果的根本原因是因为每个类的析构函数名字都不同,不满足多态的构成条件,所以不能实现指向谁就调用谁的析构函数,解决这个问题的方法就是在析构函数前加一个virtual,并且编译器会默认将所有析构函数的名字统一看做destructor,这样就构成多态的条件了。

2.4 C++11 override 和 final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮 助用户检测是否重写。

1.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

2.final:修饰虚函数,表示该虚函数不能再被重写

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

三. 抽象类

3.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

3.2接口继承和实现继承

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


接下来我看一道坑人的题目:

这个题目的答案是B

【解释】:

从代码我们可以看出,子类重写了父类的func函数,继承了父类的test函数,而test函数隐藏的this指针是A*类型的,但是p指针却是子类类型的,相当于将子类类型的指针传给了this,满足多态的构成条件,此时会指向子类的func函数体,但是由于虚函数的继承是一种接口继承,虚函数重写仅重写了实现,val的值仍然是1,所以答案为B->1

四.多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base {
public:
	virtual void Func1() { cout << "Func1()" << endl; }
	int _a;
};

有些人认为这个类中只有一个整形成员变量,大小应该为4,其实这个类的大小应该为8,因为其内部还存在一个虚函数表指针,我们通过监视窗口和内存来验证一下:

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

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;
};
  1. 派生类对象由两部分构成,一部分是父类继承下来的成员,包括虚表指针,另一部分是自己的成员。
  2. 基类对象和派生类对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?是存在代码段的(常量区)

4.2多态的原理

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

在student类重写了Person类中的BuyTicket函数,我们可以看到Mike中的虚表指针指向了Person::BuyTicket函数,而Johnson中的虚表指针指向了Student::BuyTicket函数,当Person对象调用时,就执行Person中的BuyTicket函数,当Student对象调用时,就执行Student中的BuyTicket函数

补充:

  1. 满足多态的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的,指向哪个对象就执行谁
  2. 不满足多态的函数调用在编译时就确认好了。

五.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

typedef void(*VFPTR) (); 
void PrintVTable(VFPTR vTable[]) {
	for (int i = 0; vTable[i] != nullptr; ++i) {
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}    cout << endl;
}

int main()
{
	Base b;
	Derive d;
	//1.先取b的地址,强转成一个int * 的指针 
	//2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	//3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);   
	PrintVTable(vTabled);
	return 0;
}

可以看出,子类重写了func1函数,将虚表中的func1进行了覆盖,继承了父类的func2函数,子类自己的虚函数按申明顺序依次填入虚表中

5.2 多继承中的虚函数表

从监视窗口我们可以看出,d对象存在两个虚表,这是因为Derive继承了两个父类,也继承了两个父类的虚表,由于监视窗口不全,我们在通过代码打印一下这两个虚表

int main()
{
	Derive d;
    //虚表的前四个字节
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);   
	PrintVTable(vTabled);
    
    //跳过Base1,找第二个虚表
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));   
	PrintVTable(vTableb2);
	return 0;
}

多继承派生类重写了基类的虚函数就在对应的虚表进行覆盖,未重写的虚函数放在第一个继承基类部分的虚函数表中

5.3 菱形继承、菱形虚拟继承

菱形继承:

菱形继承的对象模型跟多继承类似

菱形虚拟继承:

B、C继承了A的虚表,分别放自己的重写与未重写的虚函数,但是由于A是公共的,他的虚函数不能放到BC的虚表中,所以A自己还会存在一个虚表,此外BC中还会存在虚基表指针,派生类未重写的虚函数会放到第一个继承的类(B)的虚表中

六、关于多态的面试题

1. 什么是多态?答:多种形态,不同对象执行相同操作结果不同

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

3. 多态的实现原理?

4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。

8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基 表搞混了。

11. 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。

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

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

相关文章

ChatGPT变懒原因:正在给自己放寒假!已被网友测出

ChatGPT近期偷懒严重&#xff0c;有了一种听起来很离谱的解释&#xff1a; 模仿人类&#xff0c;自己给自己放寒假了&#xff5e; 有测试为证&#xff0c;网友Rob Lynch用GPT-4 turbo API设置了两个系统提示&#xff1a; 一个告诉它现在是5月&#xff0c;另一个告诉它现在是1…

【go项目01_学习记录03】

学习记录 1 路由http.ServeMux1.1 查看HandleFunc方法源码1.2 查看ListenAndServe方法源码1.3 重构&#xff1a;使用自定义的 ServeMux1.4 http.ServeMux 的局限性1.4.1 URI 路径参数1.4.2 请求方法过滤1.4.3 不支持路由命名 1.5 http.ServeMux 的优缺点 1 路由http.ServeMux …

当下大模型的趋势以及如何让学习大模型?

当下大模型的趋势 近年来&#xff0c;随着计算能力的提升、数据量的增加以及算法的进步&#xff0c;大模型在人工智能领域展现出了显著的发展趋势。以下是截至2024&#xff0c;大模型发展的一些关键趋势&#xff1a; 参数规模持续增长&#xff1a;从OpenAI的GPT-3的1750亿参数…

森林消防的新利器:高扬程水泵的应用与优势/恒峰智慧科技

森林是地球上的绿色肺叶&#xff0c;保护森林安全对于维护生态平衡和人类生存环境至关重要。在森林消防领域&#xff0c;高效、快速的灭火设备是保障森林安全的重要武器。近年来&#xff0c;高扬程水泵作为一种新型的消防设备&#xff0c;在森林消防中发挥了重要作用。本文将详…

python多标签图像分类的图片相册共享交流系统vue+django

建立图片共享系统&#xff0c;进一步提高用户对图片共享信息的查询。帮助用户和管理员提高工作效率&#xff0c;实现信息查询的自动化。使用本系统可以轻松快捷的为用户提供他们想要得到的图片共享信息。 根据本系统的基本设计思路&#xff0c;本系统在设计方面前台采用了pytho…

IPO压力应变桥信号处理系列隔离放大器 差分信号隔离转换0-10mV/0-20mV/0-±10mV/0-±20mV转4-20mA/0-5V/0-10V

概述&#xff1a; IPO压力应变桥信号处理系列隔离放大器是一种将差分输入信号隔离放大、转换成按比例输出的直流信号混合集成厚模电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等行业。该模块内部嵌入了一个高效微功率的电源&#xff0c;向输入端和输出端…

ubuntu20.04 手动配置docker下autoware.universe环境

使用docker手动安装autoware环境&#xff0c;参考文章&#xff0c;中间踩过很多坑&#xff0c;特此记录一下。我电脑配置如下&#xff0c;有同样配置的小伙伴可以参考安装&#xff1a; ubuntu20.04cuda: cuda-11.6ros2: foxy 一、手动安装ros2、cuda等 1.1 ROS2安装 推荐使用…

04-23 周二 shell环境下读取使用jq 读取json文件

04-23 周二 shell环境下读取使用jq 读取json文件 时间版本修改人描述04-23V0.1宋全恒新建文档 简介 工具列表 Shell脚本处理JSON数据工具jq jshon是另外一个读取json数据的工具 而且其支持XML和YAML格式文件 linux shell环境下处理yml文件 #!/bin/bash# 加载shyaml库 . /…

在java类前添加上文档注释

第一步&#xff1a; 第二步 第三步 将下面代码粘上 /** *Author Lnn *Date ${DATE}/${TIME} *ClassName ${NAME} *Description */

限量背包问题

问题描述 限量背包问题&#xff1a;从m个物品中挑选出最多v个物品放入容量为n的背包。 问题分析 限量背包问题&#xff0c;可以用来解决许多问题&#xff0c;例如要求从n个物品中挑选出最多v个物品放入容量为m的背包使得背包最后的价值最大&#xff0c;或者总共有多少种放法…

先进制造业数字化转型,为什么基于传统存储无法完成?

本文是 XSKY 智能存储方案助力先进制造数字化转型系列文章中的第一篇&#xff0c;重点分享先进制造行业数字化转型过程中&#xff0c;对于数据存储的需求&#xff0c;以及为何传统存储架构无法很好满足这些需求。 随着智能制造的发展&#xff0c;自动化、信息化、智能化等技术…

unity基础(二)

debug方法 Debug.Log(" 一般日志 ");Debug.LogWarning(" 警告日志 ");Debug.LogError(" 错误日志 ");// Player Informationstring strPlayerName "Peter";int iPlayerHpValue 32500;short shPlayerLevel 10;long lAdvantureExp 1…

k8s部署Kubeflow v1.7.0

文章目录 环境介绍部署访问kubeflow ui问题记录 环境介绍 K8S版本&#xff1a;v1.23.17&#xff0c;需要配置默认的sc 参考&#xff1a;https://github.com/kubeflow/manifests/tree/v1.7.0 部署 #获取安装包 wget https://github.com/kubeflow/manifests/archive/refs/tag…

【方法】如何创建RAR格式压缩文件?

为了方便存储或者传输文件&#xff0c;我们经常会把文件打包成不同格式的压缩包&#xff0c;那如果想创建的是RAR格式的压缩包&#xff0c;要如何做呢&#xff1f; RAR是WinRAR软件独有的压缩格式&#xff0c;所以我们可以通过WinRAR软件来创建RAR格式压缩包。下面分享两种创建…

5000亿参数来了:微软将推出 MAI-1 模型硬刚谷歌和OpenAI|TodayAI

美国的科技巨头微软公司&#xff0c;正在积极扩展其人工智能&#xff08;AI&#xff09;技术的领域。最新消息显示&#xff0c;微软将推出一款名为MAI-1的全新AI模型&#xff0c;其规模巨大&#xff0c;预计将拥有5000亿个可调参数。这一开发工作由Inflection AI的CEO穆斯塔法苏…

cmake进阶:变量的作用域说明三(从函数作用域方面)

一. 简介 前一篇文章从函数作用域方面学习了 变量的作用域。文章如下&#xff1a; cmake进阶&#xff1a;变量的作用域说明一&#xff08;从函数作用域方面&#xff09;-CSDN博客cmake进阶&#xff1a;变量的作用域说明二&#xff08;从函数作用域方面&#xff09;-CSDN博客…

在Node.js(express 框架)中使用 JWT 进行身份认证

文章目录 一、JWT 认证机制二、安装 JWT 相关的包三、基本使用1、生成 JWT 字符串2、添加中间件&#xff0c;解析 JWT 字符串3、获取管理员信息(admin) 一、JWT 认证机制 JWT 认证机制&#xff08;图片来源于网络&#xff0c;侵权删除&#xff09;&#xff1a; 关于 JWT 原理可…

Wish、Newegg、Allegro卖家如何做测评补单 快速提升产品权重与销量

大部分主流平台卖家都会使用测评补单来增加产品权重、提高销量。经常会有一些平台的卖家咨询我其他平台能否像亚马逊一样通过测评补单来提升曝光。 其实大部分跨境电商都是可以通过补单来增加店铺权重提升产品排名。其实亚马逊相对来说风控是最严的&#xff0c;风控点多达几十…

Pytorch基础:内置类type的用法

相关阅读 Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm1001.2014.3001.5482 在python中&#xff0c;一切数据类型都是对象&#xff08;即类的实例&#xff09;&#xff0c;包括整数、浮点数、字符串、列表、元组、集合、字典、复数、布尔、函数、…

Telnet的三种配置和SSH配置

Telnet的三种配置 实验配置思路&#xff1a; 配置接口IP地址&#xff1a; R1——配置接口IP地址 R2——配置接口IP地址 认证模式为none的配置 R1——认证模式配置为none R2——测试Telnet连接R1设备 认证模式为passwrd的配置 R1——认证模式配置为password R2——测试Telnet连…