C++——oo的魅力之多态

news2024/11/19 9:27:17

文章目录

  • 多态的概念
  • 多态的定义和实现
    • 多态的构成条件
    • 虚函数重写的两个例外
      • 协变(基类和派生类虚函数返回值类型不同)
      • 析构函数的重写(基类和派生类析构函数名字不同)
    • c++11 `override` 和 `final`关键字
  • 重载,重写(覆盖), 隐藏(重定义)对比
  • 抽象类(纯虚函数)
  • 多态的原理
    • 虚表
    • 派生类虚表行为
    • 多态实现细节
    • 动态绑定与静态绑定
  • 多继承的虚函数表
    • 菱形继承,菱形虚继承
  • 关于多态使用的小细节

多态的概念

多态,通俗来说,就是多种形态,就是当去完成某种行为时,不同的对象会发生不同的行为。

就像学生和普通成人去景区买票,同样是买票,学生和普通成人所要花费的资金是不一样的。

多态的定义和实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生的不同的行为。如下面的例子:student继承了person,student买票半价,person买票全价。

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

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数在基类必须用virtual关键字声明,并且派生类必须对基类的虚函数进行重写(注意,这里的重写和继承中函数的隐藏(重定义)是两个概念)
    被virtual定义的函数叫做虚函数

重写形成的条件相对重定义更加苛刻,需要派生类虚函数和基类虚函数的返回值类型,函数名字,参数列表完全相同。

在这里插入图片描述

**注意:**关于在符合重写条件的情况下,可以只在基类将函数用virtual关键字修饰,而派生类该函数不用加virtual,但不能只在派生类该函数加上virtual(一般情况下建议两边都加上virtual)

虚函数重写的两个例外

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

派生类重写虚函数时,有一种情况允许其于基类虚函数返回值不同,那就是协变即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用

注意,只用同时为指针或者同时为引用能完成协变,其他类型都不行,一个指针一个引用也不行,基类和派生类返回顺序相反也不能构成协变。(即基类返回派生类的指针或引用,派生类返回基类的指针或引用也是不行的)

class person
{
public:
	virtual void buyTicket() { cout << "买票——全价" << endl; }
	virtual person& f() { return *this; }
};

class student : public person
{
public:
	virtual void buyTicket() { cout << "买票——半价" << endl; }
	virtual student& f() { return *this; }
};

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

如果基类的析构函数为虚函数,此时其和派生类的析构函数一定构成重写,虽然派生类和基类的函数名一定不相同,看起来违背了重写的规则,但实则不然,在底层,编译器都会将析构函数的名称做统一的特殊处理,编译后析构函数的名称将会统一处理成destructor()
那么为什么要支持析构函数多态呢?我们看下面的场景:

void test()
{
	person* p1 = new person;
	person* p2 = new student;
	delete p1;
	delete p2;
}

正是由于这个场景,一定要支持虚函数多态,由于基类指针可以指向派生类指针,如果不支持析构函数多态,上面的这段代码将不能正常调用派生类析构函数清理多余资源,将会导致内存泄漏问题,因此只有通过多态才能正常释放资源。

class person
{
public:
	virtual void buyTicket() { cout << "买票——全价" << endl; }
	//virtual person& f() { return *this; }
	virtual ~person() { cout << "析人\n"; }
};

class student : public person
{
public:
	virtual void buyTicket() { cout << "买票——半价" << endl; }
	//virtual student& f() { return *this;}
	virtual ~student() { cout << "析学\n"; }
};

void test()
{
	person* p = new person;
	person* s = new student;
	//将会调用基类析构
	delete p;
	//调用派生类析构释放派生类资源
	//然后调用基类析构释放基类资源
	delete s;
}
int main()
{
	test();
	return 0;
}

在这里插入图片描述

c++11 overridefinal关键字

从上面我们知道,虚函数对重写的要求很严格,需要三同(函数名相同,参数列表相同,返回值相同)以及基类指针或引用调用,但是在有些情况下容易疏忽,容易出现错误,因此c++11提供了这两个关键字帮助用户检查是否重写。
**final:**修饰虚函数,表示该虚函数不能再被重写(该关键字放在函数名括号之后)
在这里插入图片描述

**override:**检查派生类虚函数是否重写了某个虚函数,如果没有重写编译报错。
在这里插入图片描述

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

重载,重写,隐藏

抽象类(纯虚函数)

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

override的作用是检查重写,而纯虚函数的作用是强制重写。
在这里插入图片描述
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

在探究 多态原理之前,我们先来看一道常考的面试题:

//请问sizeof(base)是多少
//(32位平台下)
class base
{
public:
	virtual void func()
	{
		cout << "func()" << endl;
	}
private:
	int _b = 1;
}

虚表

通过测试我们可以发现base对象是8bytes(32位平台下),除了b成员,还有一个 _vfptr放在对象的最前面(与平台有关), 对象中的这一指针叫做虚函数表指针(v——virtual,f——function),一个含有虚函数的类中至少都有一个虚函数指针,因为虚函数的地址要放到虚函数表中,虚函数表也简称为虚表

在这里插入图片描述

注意,这里的虚表要和虚继承中解决菱形继承问题的虚基表区分开,两者是截然不同的概念,如果有不清楚虚基表和虚继承是什么的,可以看看博主的另一篇博客,链接如下:
c++_深究继承
里面关于菱形继承的部分就有为大家讲解虚继承是什么。

派生类虚表行为

那么,了解了这个之后,我们继续看看派生类在这个表中做了什么,又是如何实现多态的。
针对上面的代码,我们进行如下的改造:

class base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{
		cout << "func3()" << endl;
	}
private:
	int _b = 1;
};

class derive : public base
{
public:
	virtual void func1()
	{
		cout << "next::func1()" << endl;
	}
private:
	int _c = 2;
};

int main()
{
	base b;
	derive n;
	return 0;
}

在这里插入图片描述

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

  1. 派生类对象n中也有一个虚表指针,n对象由两部分构成,一部分是父类继承下来的成员以及虚表指针,另一部分是自己的成员
  2. 基类b对象和派生类对象虚表是不一样的,我们发现func1完成了重写,所以n的虚表里面存储的是derive::func1,而func2在派生类中并没有重写,所以派生类虚表中仍然是base::func2(),因此重写也可以叫做覆盖,覆盖就是指虚表中虚函数的覆盖,重写是语法层的叫法,覆盖是原理层的叫法。
  3. 虚表中存放的只有虚函数,也就是被声明为virtual的函数,因此在该例子中func3并没有在虚表内。
  4. 虚函数表本质上是一个存虚函数指针的指针数组,有些编译器的虚表数组最后面放了一个nullptr
  5. 接下来总结一下派生类虚表是生成过程:a. 先将基类的虚表内容拷贝一份到派生类虚表 b. 如果派生类重写了基类的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在派生类中的声明顺序依次加到派生类虚表的最后面。

对于最后一步,vs的监视窗口可能有一点小bug无法直接看到,需要用一些小技巧才能看到。
在这里插入图片描述

  1. 接下来还有一个很多同学都容易混淆的问题:虚表存在哪里呢? 网上有很多种说法,很大一部分说法说虚表存在数据段中,但这种说法真的对吗???我们通过比较实验的方法来观察一下。
    在这里插入图片描述
    首先通过刚才的测试我们知道,在derive类中虚表指针是放在对象开始的,所以我们先将derive对象强转成int*然后对齐解引用就拿到了虚表的地址,通过对四个区域的数据进行比对,我们可以发现虚表的位置和代码段数据的位置相隔最近,与数据段的位置看似不远,但是16进制的第四位差别已经接近上万字节了,和虚表还是有点距离的,所以我们可以推荐虚表并不放在数据段(静态区),而放在**代码段(常量区)**中。

其实放在常量区中也是一个比较合理的选择,因为虚表是不能被随意修改的。

多态实现细节

接下来,有了虚表这个概念后,我们就可以更容易的理解多态了。
回顾一下多态需要的条件:

  • 基类指针调用
  • 派生类虚函数满足三同,构成重写

在学习了虚表之后,多态这个过程也就不那么神秘了,其实就是在用基类指针调用重写函数时,编译器会直接进入虚表内拿到所要调用的函数地址,也就是说在满足多态以后的函数调用,不是在编译的时候确定的,是运行起来以后到对象的虚表中去查找的。而不满足多态的函数调用在编译的时候早已确认好。

那么,再来思考一个问题,为什么一定要是**基类指针或引用调用?**直接用基类对象调用不行吗?

这里我们需要理解的一个至关重要的点就是引用或指针不会修改原来对象的虚表!正是由于这个原因,才必须用引用或者指针,如果函数参数是基类对象,那么将派生类对象传入时,就会修改对象虚表,从而不能达到多态的效果!

动态绑定与静态绑定

上面的内容又引出了一个概念,就是动态绑定和静态绑定

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

多继承的虚函数表

看如下多继承:

class base1
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
	virtual void func2()
	{
		cout << "func2()" << endl;
	}
	void func3()
	{
		cout << "func3()" << endl;
	}
private:
	int _b = 1;
};

class base2
{
public:
	virtual void func1()
	{
		cout << "base2::func1()\n";
	}
	virtual void func2()
	{
		cout << "base2::func2()\n";
	}
};

class derive : public base1, public base2
{
public:
	virtual void func1()
	{
		cout << "next::func1()" << endl;
	}
	virtual void func4()
	{
		cout << "func4()" << endl;
	}
private:
	int _c = 2;
};


对于多继承来说,派生类将有多个虚表(有几个带虚函数的基类就有几个虚表),如果两个基类有构成重写的函数,并且派生类也有构成重写的该函数,那么派生类的该函数指针将会同时覆盖两个基类函数的虚表内的该函数指针,另外,如果派生类中有自己新增的虚函数,将会放进第一个继承的基类的需表中,同样可以通过监视窗口操作看到。下图可以更好的说明:
在这里插入图片描述

菱形继承,菱形虚继承

在继承的学习中,我们知道为了解决菱形继承的数据冗余和二义性问题,引入了虚继承,而虚继承是用虚基表实现的,而多态是由虚表实现的,那将这两者结合起来之后,就越能感觉到c++的恐怖了,在实际中我们并不建议设计出菱形虚拟继承,一方面太复杂容易出问题,另一方面这样庞大的模型,访问基类成员有一定的性能损耗。

因此,菱形虚拟继承的虚表我们也不需要进行深究,这里带大家简单的了解一下即可。
在这里插入图片描述
可以看到虚继承+虚函数是非常复杂的,另外通过观察得知最终类的虚函数同样被放在了第一个继承的类中,而不是放在person类,当然这也跟编译器有关,本编译器是vs2022的结果。

另外,还有一个疑点就是虚基表中的第一行存放的是0xfffffc,翻译成十进制是-4,博主对于-4的作用还未能得知,如果有知道的佬欢迎在评论区解答。

由于菱形虚继承过于复杂,所以在实际应用中一定要尽量避免使用菱形虚继承,否则会造成很大的麻烦。

关于多态使用的小细节

  1. inline函数可以是虚函数,但是在编译器会忽略inline这一属性。

很合理,因为内联函数没有地址,没办法放进需表中

  1. 静态成员函数不可以是虚函数,因为静态成员函数没有this指针,无法访问虚函数表,所以不能通过运行时确定调用对象,因此没办法放入虚函数表
  2. 构造函数不能是虚函数,因为虚函数指针是在初始化列表中初始化的(和先有鸡还是先有蛋的问题很想)
  3. 虚表是在编译期间就生成了,一般存放在代码段中。

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

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

相关文章

Vivado使用入门之二:网表物理约束

目录 一、背景 二、物理约束 2.1 概念 2.2 网表约束 2.2.1 CLOCK_DEDICATED_ROUTE 2.2.2 MARK_DEBUG 2.2.3 DONT_TOUCH 2.2.4 LOCK_PINS 三、位置约束 四、布线约束 4.1 route 4.2 assign routing mode 五、参考 一、背景 在工程设计中为了保证上板后功能正常&…

【BI看板】Docker-compose安装Superset,安装最新版本2.1.0

软件及环境准备 docker&#xff0c; docker-compose docker-compose安装 字节码安装 #wget https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-linux-x86_64 #mv docker-compose-linux-x86_64 docker-compose #chmod x /usr/local/bin/docker-com…

一、计算机网络体系结构

Content 1. 计算机网络的组成2. 计算机网络的功能3. 计算机网络的分类4. 计算机网络的性能指标5. 计算机网络分层结构OSI模型TCP/IP模型互联网五层模型共同点&#xff1a; 6. 计算机网络提供的服务按三种方式分类面向连接服务和无连接服务可靠服务和不可靠服务有连接服务和无连…

5G+AI数字化智能工厂建设解决方案PPT

导读&#xff1a;原文《5GAI数字化智能工厂建设解决方案》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。数字化智能工厂定义 智能基础架构协同框架 - 端、边、云、网…

Java课题笔记~ SpringMVC拦截器

SpringMVC 中的 Interceptor 拦截器&#xff0c;它的主要作用是拦截指定的用户请求&#xff0c;并进行相应的预处理与后处理。其拦截的时间点在“处理器映射器根据用户提交的请求映射出了所要执行的处理器类&#xff0c;并且也找到了要执行该处理器类的处理器适配器&#xff0c…

2023华为产品测评官-开发者之声 + 华为云ModelArts试用体验心得

2023华为产品测评官&#xff0d;开发者之声 华为云ModelArts试用体验心得 文章目录 2023华为产品测评官&#xff0d;开发者之声 华为云ModelArts试用体验心得一、活动介绍二、华为云ModelArts简介三、AI Gallery简介步骤1&#xff1a;订阅模型步骤2&#xff1a;使用订阅模型部…

Reids 的整合使用

大家好 , 我是苏麟 , 今天带来强大的Redis . REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统&#xff0c;是跨平台的非关系型数据库。 Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选…

冯·诺依曼计算机

一、定义 冯诺依曼机&#xff08;von Neumann machine&#xff09;&#xff0c;又称冯诺依曼计算机&#xff0c;根据冯诺依曼提出的存储程序概念设计的计算机。主要特征是&#xff1a;指令与数据都以二进制形式储存在存储器里&#xff1b;指令根据其储存的顺序执行。 冯…

SpringBoot常用注解 - @Controller

Controller : Controller是加在类上面的注解&#xff0c;使得类里面的每个方法都返回一个视图页面 实际开发中&#xff0c;有时候只是让后端的结果返回到前端&#xff0c;而不作为新的视图页面&#xff0c;此时需要结合 ResponseBody&#xff0c;让这个方法返回给前端的不是一个…

三星霸主地位“无可撼动“,DRAM内存市场份额创近 9 年新低仍第一

三星电子在DRAM市场的竞争地位一直备受关注。据报告显示&#xff0c;除了市场份额下降外&#xff0c;三星电子在上半年的销售额也出现了下滑。这主要是由于全球消费电子产品需求下滑&#xff0c;导致三星电子的芯片需求减少。 存储芯片业务所在的设备解决方案部门的营收和利润也…

快速提高写作生产力——使用PicGo+Github搭建免费图床,并结合Typora

文章目录 简述PicGo下载PicGo获取Token配置PicGo结合Typora总结 简述PicGo PicGo: 一个用于快速上传图片并获取图片 URL 链接的工具 PicGo 本体支持如下图床&#xff1a; 七牛图床 v1.0腾讯云 COS v4\v5 版本 v1.1 & v1.5.0又拍云 v1.2.0GitHub v1.5.0SM.MS V2 v2.3.0-b…

Python_数据容器详解

Python数据容器 1. 列表基础语法和操作练习题 2. 列表的循环练习题 3. 元组 tuple4. 元组的循环练习题 5. 字符串6. 切片练习总结 7. set 集合8. 字典 dict字典的嵌套总结 字典常用操作练习 9. 对比总结以及通用操作对比总结通用操作 1. 列表基础语法和操作 """…

蓝桥杯嵌入式省一教程:(二)LCD显示

在嵌入式开发中&#xff0c;屏幕显示是一个非常重要的功能。同时&#xff0c;其移植对于初学者来说较为复杂&#xff0c;需要较好地掌握I2C或SPI等通讯协议。然而&#xff0c;在蓝桥杯中&#xff0c;比赛方已经为我们提供了与LCD有关的库&#xff0c;这让我们能够简单方便地使用…

Nginx 下载、安装与运行

下载地址 Nginx官网 - 下载页面 在Windows电脑&#xff0c;下载Windows版本。 下载的就是一个 zip 压缩包。解压后的文件就是可以直接使用的 Nginx 。 版本说明 选择 Mainline 版本就可以了。 解压到合适的位置 运行Nginx 命令行&#xff0c;进入 Nginx 所在的目录。运…

产品经理:能不能把 Vue 的中文输入法 bug 解决了?

前言 有个挺常见的需求相信大家应该都遇到过&#xff0c;就是一个搜索框&#xff0c;边输入边提示&#xff0c;类似于下面这样&#xff1a; 这玩意在前端也挺好实现的&#xff0c;就 v-model 然后 watch 再做个防抖请求接口呗&#xff01;于是我&#xff1a; <template>…

SAP MM学习笔记25- SAP中 基本数量单位,发注单位,发注价格单位

SAP 的 MM Master 中有 3种单位。 1&#xff0c;基本数量单位&#xff08; 基本订单单位&#xff0c; 库存管理的最小单位&#xff09; 2&#xff0c;发注单位&#xff08;订单单位&#xff09; 3&#xff0c;发注价格单位&#xff08;订单价格单位&#xff09; 管理 SAP 库…

【jenkins】jenkins流水线构建打包jar,生成docker镜像,重启docker服务的过程,在jenkins上一键完成,实现提交代码自动构建的功能

【jenkins】jenkins流水线构建打包jar&#xff0c;生成docker镜像&#xff0c;重启docker服务的过程&#xff0c;在jenkins上一键完成&#xff0c;实现提交代码自动构建&#xff0c;服务重启&#xff0c;服务发布的功能。一键实现。非常的舒服。 1. 启动脚本 shell脚本 这是 s…

测试部门来了个00后卷王之王,老油条感叹真干不过,但是...

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&…

SystemVerilog之接口详解

1.入门实例 测试平台连接到 arbiter的例子&#xff1a;包括测试平台, arbiter仲裁器, 时钟发生器 和连接的信号。 ㅤㅤㅤ ㅤ ㅤㅤㅤㅤㅤ Arbiter里面可以自定义发送的权重&#xff0c; 是轮询还是自定义 grant表示仲裁出来的是哪一个&#xff0c;也即只有0&#xff0c;1&am…

基于springboot校园缴费管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…