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

文章目录

  • 多态的概念
  • 多态的定义和实现
    • 多态的构成条件
    • 虚函数重写的两个例外
      • 协变(基类和派生类虚函数返回值类型不同)
      • 析构函数的重写(基类和派生类析构函数名字不同)
    • 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.mzph.cn/news/47370.shtml

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

相关文章

SOLIDWORKS基准面介绍

SOLIDWORKS是一款广泛应用于机械设计领域的三维建模软件&#xff0c;其中基准面是在建模过程中必不可少的要素。本文将介绍什么是SOLIDWORKS基准面&#xff0c;以及它在设计中的作用。 SOLIDWORKS基准面是指在设计过程中用来确定草图绘制、特征创建的参考平面。 SOLIDWORKS基…

天锐绿盾安全U盘系统

安全U盘系统 01 简介 天锐绿盾安全U盘系统&#xff0c;是一款致力于保障U盘数据内容安全的产品。通过严格身份认证、便捷安全的密保机制、智能的U盘锁定或自毁设置、详细的文件操作日志、文件粉碎、设置还原等&#xff0c;天锐绿盾安全U盘系统为您U盘的数据保驾护航&#xff0…

python 打印一个条形堆积图

背景 今天介绍一个不使用 matplot&#xff0c;通过 DebugInfo模块打印条形堆积图 的方法。 引入模块 pip install DebugInfo打印销售转化数据 下面的代码构建了两个销售团队&#xff0c;团队A 和团队B&#xff1b;两个团队的销售数据构成了公司总的销售成果。以条形堆积图的…

SQL Server、MySQL和Oracle数据库分页查询的区别与联系

摘要&#xff1a;本文将通过一个现实例子&#xff0c;详细解释SQL Server、MySQL和Oracle这三种常见关系型数据库在分页查询方面的区别与联系。我们将提供具体场景下的SQL语句示例&#xff0c;并解释每个数据库的分页查询用法以及优化方法&#xff0c;帮助读者更好地选择适合自…

Apache JMeter

下载 Apache JMeter 并安装 java链接 打开 apache-jmeter-5.4.1\bin 找到jmeter.bat 双击打开 或者 ApacheJMeter.jar 双击打开 设置中文 找到 options 》choose Language 》chinese 新建 计划 创建线程组 添加Http请求 配置元件添加请求头参数&#xff08;content-type&…

什么是PPS和TOD时序?授时防护设备是什么?

介绍 PPS和TOD PPS和TOD是两种用于精确时间同步的技术&#xff0c;它们在许多领域都有广泛的应用&#xff0c;总的来说&#xff0c;PPS和TOD被广泛应用于各种需要高度精确时间同步的领域&#xff0c;包括通信、测量、测试、系统集成和计算机网络等。 一、PPS PPS&#xff08…

RedisDesktopManager 连接redis

redis查看是否启动成功 ps -ef | grep redis以上未启动成功 cd /usr/local/bin/ 切换根目录 sudo -i 开启服务端 ./redis-server /usr/local/redis/redis.conf 开启客户端 ./redis-cli

Java【SpringBoot】SpringBoot 和 Spring 有什么区别? SpringBoot有哪些优点?

文章目录 前言一、Spring 特点二、SpringBoot 特点和优点总结 前言 各位读者好, 我是小陈, 这是我的个人主页, 希望我的专栏能够帮助到你: &#x1f4d5; JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等 &#x1f4d7; Java数据结构: 顺序表, …

ElasticSearch学习2

1、索引的操作 1、创建索引 对ES的操作其实就是发送一个restful请求&#xff0c;kibana中在DevTools中进行ES操作 创建索引时需要注意ES的版本&#xff0c;不同版本的ES创建索引的语句略有差别&#xff0c;会导致失败 如下创建一个名为people的索引&#xff0c;settings&…

Java智慧工地系统源码(微服务+Java+Springcloud+Vue+MySQL)

智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台&#xff0c;是一种全新的管理模式&#xff0c;能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度&#xff0c;以及施工过程管理的进度、质量、安全三…

cdh6.3.2 Flink On Yarn taskmanager任务分配倾斜问题的解决办法

业务场景&#xff1a; Flink On Yarn任务启动 组件版本&#xff1a; CDH&#xff1a;6.3.2 Flink&#xff1a;1.13.2 Hadoop&#xff1a;3.0.0 问题描述&#xff1a; 在使用FLink on Yarn调度过程中&#xff0c;发现taskmanager总是分配在集中的几个节点上&#xff0c;集群…

记一次从Redis弱口令到RCE

Fscan扫描网段发现了一些开启了6379的服务器&#xff0c;逐个尝试了下未授权&#xff0c;然后尝试了下爆破 hydra爆破redis hydra -P [字典目录] redis://xxx.xxx.xxx.xxx结果还真让爆出来一个 得到密码后&#xff0c;连接上去&#xff0c;这里用的是Another Redis Desktop M…

飞机打方块(四)游戏结束

一、游戏结束显示 1.新建节点 1.新建gameover节点 2.绑定canvas 3.新建gameover容器 4.新建文本节点 2.游戏结束逻辑 Barrier.ts update(dt: number) {//将自身生命值取整let num Math.floor(this.num);//在Label上显示this.num_lb.string num.toString();//获取GameCo…

分布式 - 消息队列Kafka:Kafka 消费者消费位移的提交方式

文章目录 1. 自动提交消费位移2. 自动提交消费位移存在的问题&#xff1f;3. 手动提交消费位移1. 同步提交消费位移2. 异步提交消费位移3. 同步和异步组合提交消费位移4. 提交特定的消费位移5. 按分区提交消费位移 4. 消费者查找不到消费位移时怎么办&#xff1f;5. 如何从特定…

基于springboot+vue的食材商城(前后端分离)

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

Dockerfile快速搭建自己专属的LAMP环境

目录 编写Dockerfile 1.文件内容需求&#xff1a; 2.值得注意的是centos6官方源已下线&#xff0c;所以需要切换centos-vault源&#xff01; 3.Dockerfile内容 4.进入到 lamp 开始构建镜像 推送镜像到私有仓库 1.创建用户并添加到私有仓库&#xff1a;​编辑​编辑 2.推…

Android2:构建交互式应用

一。创建项目 项目名Beer Adviser 二。更新布局 activity_main.xml <?xml version"1.0" encoding"utf-8"?><LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"…

计算机网络第2章(物理层)

计算机网络第2章&#xff08;物理层&#xff09; 2.1 物理层的基本概念2.2 物理层下面的传输媒体2.2.1 导引型传输媒体2.2.2 非导引型传输媒体 2.3 传输方式2.3.1 串行传输和并行传输2.3.2 同步传输和异步传输2.3.3 单向通信&#xff08;单工&#xff09;、双向交替通信&#x…

[ubuntu]ubuntu安装vncserver后,windows连接灰屏解决方法

修改配置文件~/.vnc/xstartup为如下内容&#xff1a; #!/bin/bash export $(dbus-launch) export XKL_XMODMAP_DISABLE1 unset SESSION_MANAGERgnome-panel & gnome-settings-daemon & metacity & nautilus & gnome-terminal &# [ -x /etc/vnc/xstartup…

并查集 size 的优化(并查集 size 的优化)

目录 并查集 size 的优化 Java 实例代码 UnionFind3.java 文件代码&#xff1a; 并查集 size 的优化 按照上一小节的思路&#xff0c;我们把如下图所示的并查集&#xff0c;进行 union(4,9) 操作。 合并操作后的结构为&#xff1a; 可以发现&#xff0c;这个结构的树的层相对…