深入篇【C++】基于面向对象特性之<多态>总结->分析底层实现原理附代码案例

news2024/12/28 20:27:25

深入篇【C++】基于面向对象特性之<多态>总结->分析底层实现原理附代码案例

  • Ⅰ.多态概念理解
  • Ⅱ.多态实现条件
  • Ⅲ.多态实现原理
    • ①.虚表概念
    • ②.虚表继承
    • ③.虚表位置
  • Ⅳ.单继承和多继承关系的虚表

Ⅰ.多态概念理解

1.多态就是多种状态,当完成某种行为时,当不同对象完成时,会产生不同的状态。即不同的对象传递过去,会调用不同的函数。
比如说当我们在12306上购买高铁票时,不同的人群购买的价格是不同的,成人是全票,学生是半票,军人可以优先购票等。这就是多态行为。
2.多态调用与普通函数调用不同。多态调用看的是指向的对象,而普通调用看的是当前的类型。

Ⅱ.多态实现条件

1.在函数前面加上关键字virtual,就变成虚函数了,而且只有成员函数可以变成虚函数。

①在虚函数后面加上=0,就会变成纯虚函数,而包含是纯虚函数的类叫做抽象类。抽象,顾名思义,没有对应的实体,也就是抽象类无法实例化出对象。
②就算这个抽象类被继承下来,派生类也无法实例化出对象。
③只有当这个纯虚函数被重写,这个派生类才可以实例化对象。无法定义出一个对象,但可以定义指针或者别名。
我们可以理解抽象类的出现就是为了强制派生类对虚函数重写,实现多态。

2.重写:基类和派生类中有相同虚函数时,对派生类的的同名虚函数编写就叫做重写。
这里我们将重写,隐藏,重载比较一下:

重写:基类与派生类中具有相同的虚函数,派生类中的同名虚函数构成重写。不过要求三同(返回类型,函数名字,参数类型都相同)。
隐藏:基类和派生类中具有相同的同名函数,派生类中即可构成隐藏。
重载:要求在同一作用域内,才可以构成重载,同名函数参数也要相同,就构成重载。

class person
{
public:
	virtual void Buy()const//虚函数
	{
		cout << "全票" << endl;
	}
};

class student :public person
{
public:
	virtual void Buy()const//派生类中存在与基类同名的虚函数,并且三同,构成了重写
	{
		cout << "半票" << endl;
	}
};

2.多态实现有两个条件:
①调用的函数必须是重写的虚函数。
②调用函数的参数必须是基类的指针或引用。

void func(const person* p)//必须用基类的指针或引用
{
	p->Buy();      
}

int main()
{
	person p;
	func(&p);//指向的对象是p
	
	student s;
	func(&s);//指向的对象是s
}

在这里插入图片描述
3.虚函数重写中一些细节:
①派生类中的重写虚函数可以不用加virtual。
②重写中存在着一个特例(协变)重写的虚函数返回值可以不同,但要求必须是父子类的指针或别名。

4.析构函数可以是虚函数吗?为什么呢?
我们要理解虚函数的出现就是为了完成多态,那么析构函数可以变成虚函数吗?
答案是可以的。在析构函数的前面加上virtual就可以变成虚函数了,并且析构函数也可以构成重写!
可能你很奇怪,每个类的析构函数名字都不一样,怎么能构成重写呢?这里是因为编译器做了统一改变,将析构函数名字都改成destructor,所以可以构成重写。
那么问题又来了,那析构函数可以实现多态吗?能构成重写那么应该可以实现多态,实现多态有两个条件,还有一个条件就是:调用函数的参数必须是基类的指针或引用。
这里明确的可以告诉你,析构函数必须要求可以实现多态,因为某些场景下,如果不实现多态的话就会出bug。

class person
{
public:
	virtual void Buy()const//虚函数
	{
		cout << "全票" << endl;
	}
	virtual ~person()//析构函数要求重写
	{
		cout << "~person()" << endl;
	}
};

class student :public person
{
public:
	virtual void Buy()const
	{
		cout << "半票" << endl;
	}
	virtual ~student()//派生类的前面也可以不用加virtual
	{
		cout << "~student" << endl;
		delete[] ptr;
	}

protected:
	int* ptr = new int[10];
};

//析构函数可以成为虚函数
//析构函数前面加上virtual就构成函数重写,因为所有析构函数都改成相同
//名字destructor了,为什么要重写呢?
//有一种场景:
int main()
{

	person* p = new person;
	p->Buy();
	delete p;
    //这里是没有问题的
	p = new student;
	
	p->Buy();
	delete p;//这时如果不重写析构函就存在问题了,p指向的是派生类对象,而析构的是基类对象。
	//这里析构的是p,p是person类型的,所以还是调用的是person类的析构函数
	//这里就调用两次了,我们想要析构的是student ,是p指向的对象,
	//我们想的是它调用指向对象的函数。
	//这里我们期望p->析构 ,是一个多态调用,而不是一个普通调用。
	return 0;
}

所以你应该明白为什么要将析构函数的名字统一改成destructor了,就是为了能让析构函数可以实现多态来修补上面的bug。所以还有一个建议那就是所有需要继承的基类的析构函数前面都加上virtual,而派生类的析构函数就可以不用加了。

5.final关键字的作用:
①修饰类不能被继承。
②修饰类虚函数不能被重写。

class car
{
public:
	virtual void Drive() final //final修饰的虚函数不能重写
	{
	}
};`在这里插入代码片`

class benz : public car
{
public:
	void Drive()//被final修饰后无法重写,这里会报错的
	{
		cout << "舒适" << endl;
	}
};

根据final关键字我们可以设计出一个不能被继承的类:直接加上final关键字。而在final
关键字没有出现之前,大佬们都是这样设计的:将不想被继承的类的构造函数或者析构函数变成私有,那么任何函数都访问不到它的构造或析构,那么就无法继承下去。

6.override关键字的作用:
①检查要重写的虚函数是否重写成功,如果没有则会报错

class car
{
public:
	virtual void Drive() 
	{
	}
};

class benz : public car
{
public:
	
	void Drive() override//帮助派生类检查是否完成重写,如果没有重写则会报错
	{
		cout << "舒适" << endl;
	}
};

Ⅲ.多态实现原理

①.虚表概念

.1. 当成员函数变成虚函数时,对象内存中会生成一个虚函数表,里面存的虚函数地址,指向对应的虚函数。这个虚函数表,本质上就是一个指针,会计入对象的大小中。
在这里插入图片描述

class B
{
	virtual void fun1()
	{
		cout << "fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "fun2()" << endl;
	}

	void fun3()
	{
		cout << "fun3()" << endl;
	}

protected:
	char _b=1;
};
//类里有虚函数就会存在虚表,虚表里存的是虚函数的地址,而虚表本质上就是一个指针
int main()
{
	B bb;
	//B对象里,有着一个指针和一个char类型的字符,根据内存对齐算出来大小是8
	cout << sizeof(B) << endl;
}

②.虚表继承

1.子类的虚表是如何形成的呢?
①首先继承父类的虚表。
②将重写的虚函数地址覆盖到虚表上。
③将子类的其他虚函数地址放入虚表最后中。

2.多态是如何实现指向父类调用父类函数,指向子类调用子类函数的呢?
当指向父类对象时,就会去父类对象的虚表里找虚函数,当指向子类对象时,就会去子类对象的虚表里找虚函数。
中间发生了切割,本质上都是指向了父类数据,看到的还是父类对象。因为派生类继承不仅继承父类的所有数据,也将父类的虚表继承下来了。派生类会将重写的虚函数地址覆盖原来的基类的虚函数。这样就可以实现指向父类调用父类函数,指向子类调用子类函数。
在这里插入图片描述

3.我们要理解子类是不会产生虚表的,子类的虚表都是从父类那里继承下来的,这个虚表都是存着父类的虚函数地址,然后子类会将重写的虚函数地址覆盖到对应的虚表位置,最后会将子类的其他虚函数地址放在这个虚表的最后面。

4.要理解将子类赋值给父类对象时,切片过程中,子类的虚表并没有拷贝切过去。这个过程是不会拷贝子类的虚表的。因为如果拷贝子类的虚表赋值给父类了,那么当指向的对象是这个父类时,到这个父类的虚表里找,找的那就是子类的虚函数了,而不是父类的虚函数。

class person
{
public:
	virtual void Buy()const//虚函数
	{
		cout << "全票" << endl;
	}
};

class student :public person
{
public:
	virtual void Buy()const
	{
		cout << "半票" << endl;
	}
};

void func(const person* p)
{
	p->Buy();      //必须用基类的指针或引用
}

int main()
{
	person p;	
	student s;
	p = s;
	//将子类s对象赋给父类对象p,不会拷贝虚表的
	//当指向是父类还是会去父类的虚表里去找虚函数
	func(&p);//指向的对象是p
}

在这里插入图片描述

5.理解虚函数为什么要重写?
只有虚函数重写,派生类的虚表里才可以存真正派生类虚函数,因为这个虚表是从父类继承下来的,里面都是父类的虚函数地址。而只有派生类虚函数重写后,才可以将重写的虚函数地址覆盖上去。这样就可以做到指向父类调用父类虚表中对应的虚函数,指向子类,调用子类虚表中对应的虚函数。
6.理解接口继承和实现继承
普通函数的继承是实现继承,派生类继承基类的函数,可以使用基类的函数,继承的是这个函数的实现。
而虚函数的继承是一种接口继承,派生类继承的是虚函数的接口,目的就是为了重写函数,为了实现多态。
当虚函数重写时,重写的是函数内部的实现,而外部的框架还是是继承父类的外壳。
所以不实现多态就不要将函数弄成虚函数。

7.静态多态与动态多态:
①静态多态,程序在编译时就确定了程序行为比如函数重载。
②动态多态,程序在运行时根据拿到的类型而确定程序的行为比如函数重写与多态。

当符合多态时,运行时到指向对象的虚表中找调用函数的地址。
而当不符合多态时,直接在编译时就确定了调用函数的地址。

③.虚表位置

1.虚表通常是存在常量区的,虚表是不能修改的,同一类型的对象共用一个虚表。

Ⅳ.单继承和多继承关系的虚表

1.单继承中的虚表

class Base
{
public:
	virtual void fun1()
	{
		cout << "base:fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "base:fun2()" << endl;
	}
protected:
	int _c;
};
class Derive :public Base
{
public:
	virtual void fun1()//重写fun1
	{
		cout << "Derive:fun1()" << endl;
	}
	virtual void fun3()//虚函数fun3
	{
		cout << "Derive:fun3()" << endl;
	}
protected:
	int _d;
};
int main()
{
	Base b;
	Derive d;

}

在这里插入图片描述
这个其实是编译器的问题,它把fun3隐藏起来了,正常来说fun3是派生类的虚函数,会放在派生类的虚函数表中最后面,但发现虚表中并没有。
我们要知道fun3是放在虚表的后面就可以了,也有办法将这个虚表打印出来,但有点麻烦。所以这里就不打印了。

2.多继承中的虚表

class Base1
{
public: 
	virtual void fun1()
	{
		cout << "base1:fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "base1:fun2()" << endl;
	}
protected:
	int _b;
};
class Base2
{
public:
	virtual void fun1()
	{
		cout << "base2:fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "base2:fun2()" << endl;
	}
protected:
	int _c;
};
class Derive :public Base1 ,public Base2
{
public:
	virtual void fun1()//重写fun1
	{
		cout << "Derive:fun1()" << endl;
	}
	virtual void fun3()//虚函数fun3
	{
		cout << "Derive:fun3()" << endl;
	}
protected:
	int _d;
};
int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;

}

在这里插入图片描述
其实派生类的虚函数fun3存的虚表是第一个虚表,也就是Base1类中的虚表中。被编译器隐藏看不到。
在这里插入图片描述
还有一个问题,派生类继承了两个基类,所以派生类有两个虚表,派生类中对虚函数fun1进行了重写,那么重写后的虚函数地址会覆盖在原来的虚表上。这些都是没有问题的。有问题的是继承下来的Base1的虚表中重写的fun1地址和Base2的虚表中重写的fun1的地址竟然不一样。派生类中只有一个fun1,理论上这两个重写虚函数地址应该相同啊。但是这里面并不相同,而且当我们去调用这两地址时,就会发现竟然都调用了派生类中的虚函数fun1。这是什么原理呢?

想要调用派生类的函数,即需要传this指针去调用Derive d d.fun1();
本质上是用this指针去调用fun1的,而this指针是Derive类型的,一开始是指向Derive对象的起始位置。
而Base1对象正好是存放在Derive对象的起始位置上,但Base2对象却不是存放在Derive对象的起始位置上。
在这里插入图片描述

所以Base2中的fun1地址需要调整一下,需要减去一个Base1类型的距离,这样Base2*指针就可以到Derive的起始位置了,就可以正确调用fun1了。所以原理很简单,就是对Base2的虚函数fun1地址进行封装。而真正的地址是原来的地址再减去Base1对象大小。

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

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

相关文章

Vue电商项目--个人中心

个人中心二级路由搭建 配置路由 界面如上 我们现在要实现一种方式就是点击右侧的&#xff0c;左侧发生变化 <div class"order-right"><div class"order-content"><div class"title"><h3>我的订单</h3></di…

《贫穷的本质》阅读笔记

《贫穷的本质》阅读笔记 2023年8月11日在杭州小屋读完&#xff0c;对于穷&#xff0c;我可有太多想说的了。可以说自己活这么大以来&#xff0c;一直在摆脱贫穷&#xff0c;也将会穷尽一生去避免贫穷。作为一个穷人该如何去摆脱贫穷&#xff0c;我觉得没有一个确切的答案&#…

SQL | 使用通配符进行过滤

6-使用通配符进行过滤 6.1-LIKE操作符 前面介绍的所有操作符都是通过已知的值进行过滤&#xff0c;或者检查某个范围的值。但是如果我们想要查找产品名字中含有bag的数据&#xff0c;就不能使用前面那种过滤情况。 利用通配符&#xff0c;可以创建比较特定数据的搜索模式。 …

SpringSpringBoot常用注解

目录 一、核心注解二、Spring Bean 相关2.1 Autowired2.2 Component, Repository, Service, Controller2.3 RestController 与 Controller2.4 Configuration 与 Component2.5 Scope 三、处理常见的 HTTP 请求类型3.1 GET 请求3.2 POST 请求3.3 PUT 请求3.4 DELETE 请求3.5 PATC…

Mybatis查询

返回实体类&#xff0c;必须指定返回类型&#xff0c; resultType不能省略&#xff0c;并且数据库字段名与实体类不一致会填充NULL&#xff0c;实体类我们一般都是驼峰&#xff0c;数据库字段一般都是下划线&#xff0c;所以在查询的时候可以起别名解决,属性填充本质上调用的是…

XXLJOB 怎么用

目录 1、数据库执行sql语句&#xff0c;建立表 2、配置Admin &#xff0c;连接xxl_job数据库 3、启动admin&#xff0c;访问 http://localhost:8080/xxl-job-admin 4、需要定时任务的微服务里导入依赖 5、配置yml&#xff08;admin地址&#xff0c;执行器名字 端口&#x…

小红书店铺怎样开通?(含详细步骤)

目录 一、小红书店铺开通 二、小红书店铺的权益 三、小红书店铺的三大玩法功能 四、小红书店铺常见问题 五、开店具体步骤&#xff1a; 大家好&#xff0c;我是网媒智星&#xff0c;今天跟大家分享一下小红书店铺怎样开通的问题&#xff0c;下文总结了详细步骤&#xff0c…

ipad触控笔有必要买吗?ipad可用触控笔推荐

苹果原装的电容笔&#xff0c;和国产的平替容笔最大的区别就在于&#xff0c;平替电容笔在压感功能上只具有倾斜的压感&#xff0c;而并没有跟苹果电容笔同样的重力压感&#xff0c;苹果电容笔同时具有倾斜压感与重力压感。但是&#xff0c;如果你不经常使用电容笔来绘画的话&a…

品牌渠道控价常见问题有哪些

不管是哪个品牌在做控价时&#xff0c;会遇到的问题都是相通的&#xff0c;如果筛选低价、窜货链接&#xff0c;如何去治理这些链接&#xff0c;使其下架&#xff0c;或者是改价。也会有品牌需要针对渠道中的乱价问题进行提前预警或者规避&#xff0c;这些可以通过分析电商数据…

访问学者申请简历写作指南

作为许多追求学术进步的人士所希望的&#xff0c;成为一名访问学者是一个极具吸引力的机会。无论是为了深化学术研究、拓展国际视野&#xff0c;还是与优秀的学者们互动交流&#xff0c;访问学者的身份都能为您带来丰富的经验。而在申请成为一名访问学者时&#xff0c;一份精心…

版本控制工具——git

版本控制是指对软件开发过程中各种程序代码、配置文件及说明文档等文件变更的管理&#xff0c;是软件配置管理的核心思想之一。 版本控制最主要的功能就是追踪文件的变更。它将什么时候、什么人更改了文件的什么内容等信息忠实地了记录下来。每一次文件的改变&#xff0c;文件的…

利用简单的算法解决逻辑推理问题(推测名次/推理谁说谎)

该算法很简单&#xff0c;以至于我们只需要三部分就可以完成。以这一题为例&#xff1a; 我们创建一个数组arr[6] { 1 } arr[1]到arr[5]分别对应A B C E&#xff0c;数组的值对应的是他们的比赛名次。其中arr[0]是用来立个flag的(也就是说用来做标记)。 接下来我们只需要写两…

轻量的工作流引擎:告别低效,创造新高!

伴随着日益激烈的市场竞争&#xff0c;作为新时代的企业&#xff0c;如何在众多同质化竞争中脱颖而出&#xff0c;占有更多的市场份额&#xff0c;实现更大发展&#xff1f;此时此刻就需要拥有不同寻常的头脑&#xff0c;寻找不平常的路径&#xff0c;轻量的工作流引擎是低代码…

cron中文翻译工具类

实现效果 /*** cron转换中文工具类** author lixuan*/ public class CronUtil {private static final Logger LOGGER LoggerFactory.getLogger(CronUtil.class);/*** cron中文表达式*/private static final List<ValueLabelPair> HOUR_LIST generateValueLabelPairs(…

使用 Packet Tracer 查看协议数据单元

练习 2.6.2&#xff1a;使用 Packet Tracer 查看协议数据单元 地址表 本练习不包括地址表。 拓扑图 学习目标 捕获从 PC 命令提示符发出的 ping运行模拟并捕获通信研究捕获的通信从 PC 使用 URL 捕获 Web 请求运行模拟并捕获通信研究捕获的通信 简介&#xff1a; Wiresha…

Dynamics 365 实体配置各属性介绍

在主界面中,我们点击设置图标->高级设置->解决方案,即可跳转到解决方案配置页面。 解决方案的存在有两方面价值,一方面是方便我们对系统进行定制,比如新建实体。另一方面则是为了方便我们在不同的环境之间复制修改的内容,也即发布。 解决方案包配置 在解决方案包…

DERT:End-to-End Object Detection with Transformers

文章目录 摘要1、简介2、相关工作2.1、集合预测2.2、Transformer与并行解码2.3、目标检测 3、DETR模型3.1、目标检测集合预测损失3.2、DETR架构 4、实验4.1、与Faster R-CNN的对比4.2、消融4.3、分析4.4、用于全景分割的DETR 5、结论附录 AA.1、初步:多头注意层A.2、损失A.3、详…

广州华锐互动:电力VR安全体验让学员沉浸式感受安全危害

随着科技的不断发展&#xff0c;虚拟现实(VR)技术在电力安全体验中发挥着越来越重要的作用。VR技术可以提供一种沉浸式的体验&#xff0c;使学员更好地理解和掌握电气安全知识&#xff0c;从而减少意外事故的发生。 首先&#xff0c;VR技术可以模拟各种电气事故场景&#xff0…

最大交换(力扣)枚举 JAVA

给定一个非负整数&#xff0c;你至多可以交换一次数字中的任意两位。返回你能得到的最大值。 示例 1 : 输入: 2736 输出: 7236 解释: 交换数字2和数字7。 示例 2 : 输入: 9973 输出: 9973 解释: 不需要交换。 注意: 给定数字的范围是 [0, 10^8] 解题思路&#xff1a; 1、数最…

Figma高效工作秘笈:10个插件助你提效!

Figma插件本质上是遵循特定接口规范开发的小程序,对设计师来说,合理选择和使用Figma插件,可以极大地优化工作流程。不同的插件具有不同的用途,设计师可以根据实际需求选择适合的插件。市面上存在各种各样的Figma插件,初学者可能会不知所措。本文将推荐10款广受设计师欢迎、功能…