C++进阶----多态

news2024/11/15 9:23:58

1.多态的概念

1.1 概念

        多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同类型的对象去完成时会 产生出不同的状态。        

        举个例子:比如有一个基类Animal,它有两个子类Dog和Cat。每个子类都可以重写基类的方法,比如make_sound()方法。当调用make_sound()方法时,Dog类和Cat类会根据自己的实现发出不同的声音,这就是多态的体现。

        再比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。

2.多态的定义以及实现

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; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p) { p.BuyTicket(); }
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

虚函数重写的两个例外:

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

         派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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

        如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

2.4 C++11 override 和 final

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

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

class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};

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

class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

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

3. 抽象类

3.1 概念

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

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

3.2 接口继承和实现继承

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

4.多态的原理

4.1虚函数表

        如图我们可以观察到b对象的大小为8bit,正常情况下,我们可能会任务为四个字节,但是这是为什么呢?

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

#include<iostream>
using namespace std;
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
}

通过观察和测试,我们发现了以下几点问题:

        1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

         2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

         3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

         4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

        5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

         6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的 呢?实际我们去验证一下会发现vs下是存在代码段的

4.2动态绑定与静态绑定

        1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

        2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

        3.之前买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

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

5.1 单继承中的虚函数表

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

思路:

        取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
         1.先取b的地址,强转成一个int*的指针
         2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
         3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
         4.虚表指针传递给PrintVTable进行打印虚表
         5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

6. 继承和多态常见的问题

1、继承方法可以使变得富有

2、动态绑定是面向对象程序设计语言的一种机制,这种机制实现了方法的定义与具体对象无关,而对方法的调用则可以关联与具体的对象

3、继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复 用,也称为白盒复用 。组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动 态复用,也称为黑盒复用。继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封 装性的表现。

4、声明纯虚函数的类不能实例化对象,声明纯虚函数的类是虚基类。

5、派生类必须重新定义基类的虚函数。

6、基类中有虚函数,如果子类中没有重写虚函数,此时子类和积累共用一张虚表。

7. 什么是多态?

        答:多态是指相同的消息被不同类型的对象接收时产生不同的行为。

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

        答:

  •   重载(Overload):同一范围内的函数名字相同,但参数列表不同。编译器根据参数列表来区分不同的函数。
    • 重写(Override):子类重新定义了父类中的虚函数,子类的函数签名和父类的相同。
    • 重定义(Hide):子类重新定义了父类中的非虚函数,但函数签名不同,此时不会形成重写,而是新定义了一个函数,隐藏了父类的同名函数。

9. 多态的实现原理?

        答:多态的实现原理是通过虚函数和动态绑定实现的。当通过基类指针或引用调用虚函数时,会根据实际对象的类型来决定调用哪个版本的虚函数。

10. inline函数可以是虚函数吗?

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

11. 静态成员可以是虚函数吗?

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

12. 构造函数可以是虚函数吗?

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

13. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

        答:可以,并且最好把基类的析 构函数定义成虚函数。析构函数可以是虚函数,当需要基类指针或引用指向派生类对象并使用delete释放内存时,如果析构函数不是虚函数,只会调用基类的析构函数,而不会调用派生类的析构函数,可能导致资源泄露。因此,通常在基类中将析构函数声明为虚函数。

14. 对象访问普通函数快还是虚函数更快?

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

15. 虚函数表是在什么阶段生成的,存在哪的?

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

16. C++菱形继承的问题?虚继承的原理?

        答:菱形继承指的是通过两条不同的继承路径继承同一个基类,可能会导致派生类中包含两份相同基类的成员。虚继承是通过在派生类对共同基类的继承前加上virtual关键字来解决菱形继承带来的二义性问题,使得最终派生类只包含一份共同基类的成员。

17. 什么是抽象类?抽象类的作用?

        答:抽象类是包含纯虚函数的类,不能直接实例化对象,只能作为其他类的基类来派生出具体的子类。抽象类的作用是为了定义接口,规范子类的行为。

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

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

相关文章

三个关于文件操作的实例

内容是&#xff1a;通过文件操作&#xff0c;完成一些小的需求。用来查询某文件或者某个文件里面的关键字 1.例子一 &#xff08;1&#xff09;需求&#xff1a;在一个指定的目录下&#xff0c;找到包含该关键字的所有文件 &#xff08;2&#xff09;要求输入&#xff1a;一个…

C语言中的goto语句

goto label; C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。 #include <stdio.h> int main(){goto first;printf("我是你好\n");first:printf("nihao\n");second:printf("This is 2\n");return 0; } 使用goto会…

mysql其它补充

exist和in的区别 exists 用于对外表记录做筛选。 exists 会遍历外表&#xff0c;将外查询表的每一行&#xff0c;代入内查询进行判断。 当 exists 里的条件语句能够返回记录行时&#xff0c;条件就为真&#xff0c;返回外表当前记录。反之如果 exists 里的条件语句不能返回记…

Day31:单元测试、项目监控、项目部署、项目总结、常见面试题

单元测试 保证独立性。 Assert&#xff1a;断言&#xff0c;一般用来比较是否相等&#xff0c;比如 Assert.assertEquals 在JUnit测试框架中&#xff0c;BeforeClass&#xff0c;Before&#xff0c;After和AfterClass是四个常用的注解&#xff0c;它们的作用如下&#xff1a; …

Python爬虫:XPath解析爬取豆瓣电影Top250示例

一、示例的函数说明&#xff1a; 函数processing()&#xff1a;用于处理字符串中的空白字符&#xff0c;并拼接字符串。 主函数程序入口&#xff1a;每页显示25部影片&#xff0c;实现循环&#xff0c;共10页。通过format方法替换切换的页码的url地址。然后调用实现爬虫程序的…

Golang日志管理:使用log/slog实现高级功能和性能优化

Golang日志管理&#xff1a;使用log/slog实现高级功能和性能优化 简介基础使用初始化和配置日志级别 高级技巧自定义日志格式器条件日志处理 实战案例场景一&#xff1a;API请求日志记录场景二&#xff1a;错误跟踪和用户通知 性能优化优化日志记录的性能异步日志处理选择合适的…

【项目学习01_2024.05.05_Day05】

学习笔记 4.3 接口开发4.3.1 树型表查询4.3.2 开发Mapper4.3.3 开发Service4.3.4 测试Service 4.4 接口测试4.4.1 接口层代码完善4.4.2 测试接口 4.3 接口开发 4.3.1 树型表查询 4.3.2 开发Mapper 在对应的Mapper里定义一个方法 在同名的xml文件里具体定义相应的sql语句 4…

上市公司代理成本数据集(2000-2022年)

01、数据介绍 上市公司的代理成本是指因代理问题所产生的损失&#xff0c;为了解决代理问题所发生的成本。这些成本包括监督成本、约束成本和剩余损失。由于信息的不对称&#xff0c;股东无法知道经理人是在为实现股东收益最大化而努力工作&#xff0c;还是只为满足平稳的投资…

线性数据结构-手写队列-哈希(散列)Hash

什么是hash散列&#xff1f; 哈希表的存在是为了解决能通过O(1)时间复杂度直接索引到指定元素。这是什么意思呢&#xff1f;通过我们使用数组存放元素&#xff0c;都是按照顺序存放的&#xff0c;当需要获取某个元素的时候&#xff0c;则需要对数组进行遍历&#xff0c;获取到指…

定子的检查和包扎及转子的检查

线圈接好后 用摇表测试 线圈和外壳之间的绝缘性&#xff01; 测试通过后进行焊接&#xff01;&#xff0c;焊接的工具在后面的文章中会介绍&#xff01; 焊接好后&#xff0c;包绝缘管。 焊接完成后 进行星型连接&#xff0c;或者三角形连接&#xff01; 白扎带进行绑扎&…

【Android】Android应用性能优化总结

AndroidApp应用性能优化总结 最近大半年的时间里&#xff0c;大部分投在了某国内新能源汽车的某款AndroidApp开发上。 由于该App是该款车上&#xff0c;常用重点应用。所以车厂对应用性能的要求比较高。 主要包括&#xff1a; 应用冷启动达到***ms。应用热(温)启动达到***ms应…

一测知“芯”!芯片测试如何确保电子设备的“心脏”健康?

文章目录 封装&#xff1a;芯片的“铠甲”与“桥梁”测试&#xff1a;芯片质量的“守门员”《芯片封测从入门到精通》亮点内容简介作者简介目录获取方式 在高科技飞速发展的今天&#xff0c;芯片作为电子设备的心脏&#xff0c;承载着计算、控制、存储等核心功能。然而&#xf…

二.数据结构

单链表 数组实现单链表: int head; //head存储这个单链表的头结点 int value[N];//value存储结点的值 int nextt[N];//nextt存储结点的next指针 int id; //id表示当前用到的点的位置 //初始化: void Init(){head-1,id0;//链表的头节点要指向-1,当前结点位置为0 } //在…

python数据分析——在数据分析中有关概率论的知识

参数和统计量 前言一、总体二、样本三、统计抽样四、随机抽样4.1. 抽签法4.2. 随机数法 五、分层抽样六、整群抽样七、系统抽样八、统计参数九、样本统计量十、样本均值和样本方差十一、描述样本集中位置的统计量11.1. 样本均值11.2. 样本中位数11.3. 样本众数 十二、描述样本分…

分层解耦(IOC-DI引入)

目录 一、为什么要解耦 二、示例分析 三、如何解除耦合&#xff1f; 四、控制反转和依赖注入-简述 一、为什么要解耦 内聚&#xff1a;软件中各个功能模块内部的功能联系耦合&#xff1a;衡量软件中各个层/模块之间的依赖、关联的程度软件设计原则&#xff1a;高内聚低耦合…

FilterListener详解

文章目录 MVC模式和三层架构MVC模式三层架构MVC和三层架构 JavaWeb的三大组件Filter概述快速入门过滤器API介绍过滤器开发步骤配置过滤器俩种方式修改idea的过滤器模板 使用细节生命周期拦截路径过滤器链 案例统一解决全站乱码问题登录权限校验验 ServletContextServletContext…

Java项目:基于SSM框架实现的高校专业信息管理系统设计与实现(ssm+B/S架构+源码+数据库+毕业论文+PPT+开题报告)

一、项目简介 本项目是一套基于SSM框架实现的高校专业信息管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、…

基于51单片机PWM控制直流电机—数码管显示

基于51单片机PWM控制直流电机 &#xff08;仿真&#xff0b;程序&#xff0b;设计报告&#xff09; 功能介绍 具体功能&#xff1a; 1.L298驱动直流电机&#xff1b; 2.数码管显示转动方向和PWM占空比&#xff08;0-100%&#xff09;&#xff1b; 3.按键控制PWM占空比来加/…

Centos7网络处理name or service not known

1、编辑->虚拟网络编辑器 2、查看本机的ip 3、 /etc/sysconfig/network-scripts/ 查看文件夹下面的 ifcfg-eth33 后面的33可能不一样 vi /etc/resolv.conf 编辑文件添加以下DNS nameserver 114.114.114.114 4、设置本机的网络 5、ping www.baidu.com 先重启…

交叉导轨维护和保养的方法!

交叉导轨系统作为一种常见的机械传动装置&#xff0c;广泛应用于各种精密机械设备中。为了确保交叉导轨系统的正常运行和延长其使用寿命&#xff0c;定期维护和保养是至关重要的。 1、清洁&#xff1a;定期清理交叉导轨表面的灰尘、油污等杂质&#xff0c;保持其清洁。在清理过…