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.rhkb.cn/news/92530.html

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

相关文章

docker安装及优化详解

目录 一、部署20版的docker 1.1 安装依赖包 1.2 设置阿里云镜像源 1.3 安装docker-ce 社区版 1.4 关闭增强机制 1.5 开启服务 1.6 设置镜像加速 1.7 网络优化 二、linux 系统中的命令 记10条(cd ls pwd mv cp ) 2.1 查询docker 版本 2.2 搜索镜像 2.3 技能点 2.…

实例038 设置窗体在屏幕中的位置

实例说明 在窗体中可以设置窗体居中显示&#xff0c;本例通过设置窗体的Left属性和Top属性可以准确设置窗体的位置。运行本例&#xff0c;效果如图1.38所示。 技术要点 设置窗体在屏幕中的位置&#xff0c;可以通过设置窗体的属性来实现。窗体的Left属性表示窗体距屏幕左侧的…

C语言 字符指针

1、介绍 概念&#xff1a; 字符指针&#xff0c;就是字符类型的指针&#xff0c;同整型指针&#xff0c;指针指向的元素表示整型一样&#xff0c;字符指针指向的元素表示的是字符。 假设&#xff1a; char ch a;char * pc &ch; pc 就是字符指针变量&#xff0c;字符指…

Linux安装Docker

一、Docker系统版本介绍 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 或 Windows 操作系统的机器上&#xff0c;也可以实现虚拟化。 容器是完全使用沙箱机制&#xff0c;相…

常识判断

头像 carrin&#xff5e;&#x1f47b; 产品经理 225/753 75/302.5 30/152 15/101.5 等差数列&#xff0c;所以最后一个是10/101 收起 60 回复 发布于 2020-02-18 16:33

学习篇之React Fiber概念及原理

什么是React Fibber&#xff1f; React Fiber 是 React 框架的一种底层架构&#xff0c;为了改进 React 的渲染引擎&#xff0c;使其更加高效、灵活和可扩展。 传统上&#xff0c;React 使用一种称为堆栈调和递归算法来处理虚拟 DOM 的更新&#xff0c;这种方法在大型应用或者…

React+Typescript清理项目环境

上文 创建一个 ReactTypescript 项目 我们创建出了一个 React配合Ts开发的项目环境 那么 本文 我们先将环境清理感觉 方便后续开发 我们先来聊一下React的一个目录结构 跟我们之前开发的React项目还是有一些区别 public 主要是存放一些静态资源文件 例如 html 图片 icon之类的 …

用于弥散加权MRI的关节各向异性维纳滤光片研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

CentOS系统环境搭建(三)——Centos7安装DockerDocker Compose

centos系统环境搭建专栏&#x1f517;点击跳转 Centos7安装Docker&Docker Compose 使用 yum 安装Docker 内核 [rootVM-4-17-centos ~]# uname -r 3.10.0-1160.88.1.el7.x86_64Docker 要求 CentOS 系统的内核版本高于 3.10 更新 yum yum update安装需要的软件包&#x…

为什么商业基础软件需要开源

Bytebase 本身是一家商业软件公司&#xff0c;而作为最核心资产的代码从 Day 0 却是开源的。同时我们还是 star-history.com 的运营者&#xff0c;大家在各种开源渠道会看到它生成的图&#xff1a; 一直以来&#xff0c;常会被别人问起的一个问题&#xff0c;就是为什么 Byteba…

使用维纳过滤器消除驾驶舱噪音(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

QT QLCDNumber 使用详解

本文详细的介绍了QLCDNumber控件的各种操作&#xff0c;例如&#xff1a;新建界面、源文件、设置显示位数、设置进制、设置外观、设置小数点、设置溢出、显示事件、其它文章等等操作。 实际开发中&#xff0c;一个界面上可能包含十几个控件&#xff0c;手动调整它们的位置既费时…

Java:PO、VO、BO、DO、DAO、DTO、POJO

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Java&#xff1a;PO、VO、BO、DO、DAO、DTO、POJO PO持久化对象&#xff08;Persistent Object&#xff09; PO是持久化对象&#xff0c;用于表示数据库中的实体或表…

C语言 棱形图案

目录 一、问题分析 上部分&#xff1a; 下部分&#xff1a; 二、代码演示 一、问题分析 如上图所示&#xff0c;我们可以将棱形进行拆解&#xff0c;分为上下两个部分。 上部分&#xff1a; 通过观察&#xff0c;我们得到 单边空格数 上半部分总行数 - 行数 - 1 …

WebRTC音视频通话-WebRTC本地视频通话使用ossrs服务搭建

iOS开发-ossrs服务WebRTC本地视频通话服务搭建 之前开发中使用到了ossrs&#xff0c;这里记录一下ossrs支持的WebRTC本地服务搭建。 一、ossrs是什么&#xff1f; ossrs是什么呢&#xff1f; SRS(Simple Realtime Server)是一个简单高效的实时视频服务器&#xff0c;支持RTM…

vue基础知识五:请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢?

一、生命周期是什么 生命周期&#xff08;Life Cycle&#xff09;的概念应用很广泛&#xff0c;特别是在政治、经济、环境、技术、社会等诸多领域经常出现&#xff0c;其基本涵义可以通俗地理解为“从摇篮到坟墓”&#xff08;Cradle-to-Grave&#xff09;的整个过程在Vue中实…

pg 简单查询语句

语法&#xff1a; 搜索语句&#xff1a; select (distinct&#xff08;去重&#xff09;) 内容&#xff08;*代表所有&#xff09; as 别名 from 表 注释&#xff1a; -- 快速查询&#xff1a;select 内容 AS 别名 没有表一般当做计算器来用

清风数学建模——拟合算法

拟合算法 文章目录 拟合算法概念 确定拟合曲线最小二乘法的几何解释求解最小二乘法matlab求解最小二乘法如何评价拟合的好坏计算拟合优度的代码 概念 在前面的篇幅中提到可以使用插值算法&#xff0c;通过给定的样本点推算出一定的曲线从而推算出一些想要的值。但存在一些问题…

基于OFDM+64QAM系统的载波同步matlab仿真,输出误码率,星座图,鉴相器,锁相环频率响应以及NCO等

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 2.1 OFDM原理 2.2 64QAM调制 2.3 载波同步 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022a 3.部分核心程序 ............................................…

AI Chat 设计模式:15. 桥接模式

本文是该系列的第十五篇&#xff0c;采用问答式的方式展开&#xff0c;问题由我提出&#xff0c;答案由 Chat AI 作出&#xff0c;灰色背景的文字则主要是我的一些思考和补充。 问题列表 Q.1 如果你是第一次接触桥接模式&#xff0c;那么你会有哪些疑问呢&#xff1f;A.1Q.2 什…