【C++】C++面向对象编程三大特性之一——多态

news2024/9/29 15:34:04

❤️前言

        继上篇继承的知识之后,本片博文主要和大家一起继续学习多态的知识。多态的实现依附于继承,是面向对象的重要特性。

正文

        多态,顾名思义就是多种状态。简单来说,不同类型的对象进行相同的操作会产生不同的结果。举实例来说就是,不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person,可能Person对象买票全价,Student对象买票半价。

多态的定义和实现

        在继承中要构成多态需要两个条件:

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

虚函数

        虚函数是指被virtual关键字修饰的类成员函数。而虚函数的重写(覆盖)则是指:派生类中有个跟基类在函数原型上完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),这样就称子类的虚函数重写了基类的虚函数。

        重写有一个很容易忽略的点,虚函数重写只重写了函数实现,但没有改变函数原型,这时如果参数具有缺省值就会有点差异

        我们按照上面构成多态的两个条件使用多态去实现买票的情形:

        这就是多态的简单使用方式,现在大家只要根据上面的演示就可以自己编写各种各样的多态代码啦。

虚函数重写的一些细节

        虚函数重写的条件本来是虚函数加上函数原型相同,但是有一些例外:

  1. 派生类的重写虚函数可以不加virtual关键字,,但是这样的写法并不规范,建议大家都写上。
  2. 如果两个虚函数的返回值类型是互为父子关系的指针或引用的话,也可以看作重写(协变)。这种规则并不好,我们做了解即可。
  3. 析构函数的重写和多态。

        前两种情况我们可以简单的了解,第三种情况我们现在认真地进行研究。我们以问答形式进行研究。

        析构函数可以是重写虚函数吗?析构函数加上virtual是不是虚函数重写?

        回答:可以是,析构函数在函数名前加上virtual就是虚函数重写。因为析构函数的名字在编译时都被处理成 destructor 这个统一的名字。

        那么为什么要这样做呢?

        回答:为了让父子类的析构函数构成虚函数重写以解决一个场景:

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

// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2; // p->destructor() + operator delete(p)
	return 0;  // 这里必须将p的调用变成多态调用,否则无法清理Student对象自己的资源
}

        因此只要是使用了继承,我们最好将父子类的析构函数设置为虚函数。

C++11 override和final

        我们发现多态重写虚函数的规则十分严格,但是我们有时候可能疏忽而产生问题。如果我们因为重写失败而产生了错误,这种错误往往无法编译过程中发现,而是在得到运行结果之后才会发现bug,这样发现错误之后再去debug会很麻烦,于是C++11就给我们提供了override和final两个关键字来解决相关的问题。

        1.final:修饰虚函数,表示这个虚函数不能再被重写,如果继续对它进行重写,就会发生报错。这样我们 就不需要去考虑虚函数重写的问题了。使用方式如下:

        除此之外final还可以加在类名的后面,然后让这个类无法被继承,这个类被称之为最终类。

        2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。通过这个关键字,我们就可以很轻松的知道虚函数重写是否发生了错误。使用方式如下:

        我们可以看到,Benz类没有重写Drive函数,于是这里报出了编译错误。

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

抽象类

        在虚函数的后面写上 =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();
}

 接口继承和实现继承

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

多态的原理

        首先我们来看看一道简单的题目:

// 我们来看看这里的sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

        我们调试代码就可以发现这个类的一个对象的大小为八个字节,当我们调试并打开监视窗口监视Bace对象的情况,我们会发现除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。

虚表

        一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:

// 针对上面的代码我们做出以下改造
// 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. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

多态的原理

        上面研究了这么多但是好像还是没有说明多态的原理,现在让我们重新研究一下之前的买票的场景:

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

        我们调试上面的代码,并观察监视和汇编代码可以看到如下的现象:

  1. p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

静态绑定和动态绑定

        上面我们发现多态调用函数并不是在编译时确定的,这种确定的方式被称为动态绑定。相应的,还有一种相反的绑定方式称为静态绑定,简单来说:

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间由编译器确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

多继承关系的虚函数表

        如果使用了多继承,那么子类就会继承所有父类的虚表指针,并重写虚函数,除此之外,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

        通过研究以下的类,我们可以得到上述结论:

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

🍀结语

        上面就是今天多态的相关知识啦,希望能对大家有用。

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

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

相关文章

独立站不被收录的原因有哪些?

答案是&#xff1a;独立站不被收录是因为你的文章质量太差&#xff0c;建议使用GPC爬虫池促收录。 在进行Google优化的过程中&#xff0c;许多独立站长发现自己的网站没有被谷歌等搜索引擎收录。 这种情况可能会让站长们感到困惑和沮丧。 以下是一些常见的原因&#xff0c;以…

2023 年高教社杯全国大学生数学建模竞赛题目 B 题 多波束测线问题

B 题 多波束测线问题 单波束测深是利用声波在水中的传播特性来测量水体深度的技术。声波在均匀介质中作匀速直线传播&#xff0c;在不同界面上产生反射&#xff0c;利用这一原理&#xff0c;从测量船换能器垂直向海底发射声波信号&#xff0c;并记录从声波发射到信号接收的传播…

【C++精华铺】10.STL string模拟实现

1. 序言 STL&#xff08;标准模板库&#xff09;是一个C标准库&#xff0c;其中包括一些通用的算法、容器和函数对象。STL的容器是C STL库的重要组成部分&#xff0c;它们提供了一种方便的方式来管理同类型的对象。其中&#xff0c;STLstring是一种常用的字符串类型。 STLstrin…

2023国赛数学建模A题思路分析 - 定日镜场的优化设计

# 1 赛题 A 题 定日镜场的优化设计 构建以新能源为主体的新型电力系统&#xff0c; 是我国实现“碳达峰”“碳中和”目标的一项重要 措施。塔式太阳能光热发电是一种低碳环保的新型清洁能源技术[1]。 定日镜是塔式太阳能光热发电站(以下简称塔式电站)收集太阳能的基本组件&…

人工智能客服:是跨境电商未来的趋势吗?

随着跨境电商的快速发展&#xff0c;客户服务成为了商家们越来越关注的焦点。而在客户服务领域中&#xff0c;人工智能客服正逐渐崭露头角。那么&#xff0c;人工智能客服是否是跨境电商未来的趋势呢&#xff1f;本文将探讨这个问题&#xff0c;并揭示人工智能客服的潜力和优势…

CSS文字居中对齐学习

CSS使用text-align属性设置文字对齐方式&#xff1b;text-align:center&#xff0c;这样就设置了文字居中对齐&#xff1b; <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>css 水平居中</title><style>.box …

2023高教社杯 国赛数学建模C题思路 - 蔬菜类商品的自动定价与补货决策

1 赛题 在生鲜商超中&#xff0c;一般蔬菜类商品的保鲜期都比较短&#xff0c;且品相随销售时间的增加而变差&#xff0c; 大部分品种如当日未售出&#xff0c;隔日就无法再售。因此&#xff0c; 商超通常会根据各商品的历史销售和需 求情况每天进行补货。 由于商超销售的蔬菜…

Stable Diffuse 之 安装文件夹、以及操作界面 UI 、Prompt相关说明

Stable Diffuse 之 安装文件夹、以及操作界面 UI 、Prompt相关说明 目录 Stable Diffuse 之 安装文件夹、以及操作界面 UI 、Prompt相关说明 一、简单介绍 二、安装文件相关说明 三、界面的简单说明 四、prompt 的一些语法简单说明 1、Prompt &#xff1a;正向提示词 &am…

SpringBoot如何优雅的输出异常信息?

目录 一、什么是 SpringBoot 二、什么是异常 三、SpringBoot如何配置异常输出 一、什么是 SpringBoot Spring Boot 是一个开源的 Java 框架&#xff0c;用于创建独立的、可部署的基于 Spring 的应用程序。它是 Spring 框架的一种扩展&#xff0c;旨在简化 Spring 应用程序的…

C高级 Day2

课后作业&#xff1a; #!/bin/bash #!/bin/bashmkdir ~/dir mkdir ~/dir/dir1 mkdir ~/dir/dir2 cp * ~/dir/dir1/ cp *.sh ~/dir/dir2/ tar -cJf ~/dir/dir2.tar.xz ~/dir/dir2 mv ~/dir/dir2.tar.xz ~/dir/dir1/ tar -xJf ~/dir/dir1/dir2.tar.xz -C ~/dir/dir1/ tree ~/dir思…

WebSocket的那些事(5-Spring中STOMP连接外部消息代理)

目录 一、序言二、开启RabbitMQ外部消息代理三、代码示例1、Maven依赖项2、相关实体3、自定义用户认证拦截器4、Websocket外部消息代理配置5、ChatController6、前端页面chat.html 四、测试示例1、群聊、私聊、后台定时推送测试2、登录RabbitMQ控制台查看队列信息 五、结语 一、…

虹科干货 | 如何选择合适水下应用的集成电缆传感器?

一、 前言 许多工业过程都要求将传感器浸没在水中&#xff0c;传感器浸入液体时&#xff0c;必须根据其浸入的环境条件进行适当设计&#xff0c;以满足特定要求 二、 浸没在不同液体中的选择 1. 水浸 在大多数涉及水浸没的情况下&#xff0c;无论是淡水还是盐水&#xff0c;只…

抓包工具fiddler的基础知识

目录 简介 1、作用 2、使用场景 3、http报文分析 3.1、请求报文 3.2、响应报文 4、介绍fiddler界面功能 4.1、AutoResponder(自动响应器) 4.2、Composer(设计请求) 4.3、断点 4.4、弱网测试 5、app抓包 简介 fiddler是位于客户端和服务端之间的http代理 1、作用 监控浏…

Jquery会议室布局含门入口和投影位置调整,并自动截图

一、关于下载 1、文章中罗列了主要代码&#xff0c;如需使用&#xff0c;请前往CSDN下载进行下载&#xff0c;包中包含所有文件素材&#xff0c;开箱即用 2、下载链接&#xff1a;https://download.csdn.net/download/zlxls/88305636 二、有这么一个需求 1、会场进行布局&a…

行业追踪,2023-09-07

自动复盘 2023-09-07 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

Android手机防沉迷软件的基本原理

(现在手机游戏、短视频等不仅对小孩子负面影响巨大&#xff0c;连很多成年人都沉迷其中难以自拔&#xff0c;影响工作、生活、学习。这已经造成全社会性的巨大影响&#xff0c;长此以往&#xff0c;国将不国。本人仅在此以自己掌握的些许技术略尽绵薄之力&#xff0c;希望能抛砖…

基于SpringBoot的社团管理系统

基于SpringBootVue的社团管理系统&#xff0c;前后端分离 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 角色&#xff1a;普通用户、管理员 管理员&#xff1a;…

有哪些开源通用流程引擎

有哪些开源通用流程引擎 Activiti&#xff1a;Camunda&#xff1a;Flowable&#xff1a;jBPM&#xff1a;Bonita&#xff1a; 以下是一些常见的开源通用流程引擎&#xff1a; Activiti&#xff1a; Activiti 是一个轻量级的、基于 Java 的 BPM&#xff08;Business Process M…

Git 常用

1.工作区、暂存区、版本库&#xff1a; 工作区&#xff1a;就是电脑上可以看到的目录&#xff1b; 暂存区&#xff1a;英文叫 stage 或 index。一般存放在 .git 目录下的 index 文件&#xff08;.git/index&#xff09;中&#xff0c;所以我们把暂存区有时也叫作索引&#xf…

华为OD机试 - 最多颜色的车辆 - 数据结构map(Java 2022Q4 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路1、核心思想2、题做多了&#xff0c;你就会发现&#xff0c;这道题属于送分题&#xff0c;为什么这样说&#xff1f;3、具体解题思路&#xff1a; 五、Java算法源码六、效果展示1、输入2、输出 华为OD机试 2023B…