【1++的C++进阶】之多态

👍作者主页:进击的1++
🤩 专栏链接:【1++的C++进阶】

文章目录

  • 一,什么是多态?
  • 二,剖析多态的调用原理
  • 三,抽象类
  • 四,多继承中的虚函数表

一,什么是多态?

多态的定义:不同继承关系的类对象,去调用同一个函数,产生不同的行为。再说通俗点就是:一个行为,不同的对象去做会产生不同的结果。

构成多态的条件:

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写的条件:

  1. 是虚函数 (被virtual修饰的成员函数)
  2. 三同(函数名,参数,返回值)

特例:

  1. 子类虚函数不加virtual依旧构成重写。
  2. 重写的协变:返回值可以不同,但必须是父子关系的指针或引用
  3. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
    都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

而且不符合重写就是隐藏关系,这也是我们判断隐藏关系的条件之一。
2. 必须是基类的指针或引用去调用虚函数。

实例如下:

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}
protected:int _b;
};
void Test1()
{B t1;A& a1 = t1;a1.Func1();A* ptra = &t1;ptra->Func1();
}

在这里插入图片描述

上面还遗留一个问题,为什么要把析构函数重写。

在这里插入图片描述
来看以下代码:

class A
{
public:virtual ~A(){cout << "~A()" << endl;}A& operator=(const A& a){cout << "A::operator=()" << endl;return *this;}virtual void Func1(){cout << "A::Func1()" << endl;}
protected:int _a;
};class B:public A
{
public:virtual ~B(){cout << "~B()" << endl;}/*B& operator=(const B& a){A::operator=(*this);cout << "B::operator=()" << endl;return *this;}*/virtual void Func1(){cout << "B::Func1()" << endl;}
protected:int _b;
};
void Test1()
{A* ptra1 = new A;A* ptra2 = new B;delete ptra1;delete ptra2;	
}

若析构函数没有重写:
在这里插入图片描述
我们可以看到new的B类类型的对象没有调用自己的析构而是其指针类的析构函数。这就会存在空间没有释放的问题,造成内存泄漏。

若构成重写后:
在这里插入图片描述
我们可以看到new出的B对象调用了自己的析构函数。并且上一篇文章我们讲过,子类对象的析构会先调用自己的构造函数,然后调用父类的构造函数。

这里我们做一个小的总结:通过上述的两段代码,我们会发现,多态调用重写函数,指向哪个对象就去调用哪个对象的重写函数。

二,剖析多态的调用原理

首先我们先来回答这么一个问题:

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual int Func2(){cout << "A::Func2()" << endl;return 0;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}protected:int _b;
};void Test2()
{A t2;B t1;cout << sizeof(t1) << endl;}

上述代码中的sizeof的值为多少呢?
在这里插入图片描述
答案为12!!可能会有人疑惑,为什么不是8呢?
我们通过监视窗口来观察。
在这里插入图片描述
我们发现,除了子类的成员变量和继承的A的成员变量外还多了一个_vfptr。这是虚函数表指针。
那么这个表中放的是什么呢?
我们继续来看下面这段代码:

class A
{
public:virtual void Func1(){cout << "A::Func1()" << endl;}virtual int Func2(){cout << "A::Func2()" << endl;return 0;}virtual int Func4(){cout << "A::Func4()" << endl;return 0;}
protected:int _a;
};class B:public A
{
public:virtual void Func1(){cout << "B::Func1()" << endl;}virtual int Func3(){cout << "B::Func3()" << endl;return 0;}virtual int Func4(){cout << "B::Func4()" << endl;return 0;}
protected:int _b;
};typedef void(*Vfptr)();void PrintVFptr(Vfptr* arr)
{for (int i = 0; arr[i] != nullptr; ++i){printf("vfptr[%d]->%p  ", i, arr[i]);Vfptr pf = arr[i];pf();}
}void Test2()
{A t2;B t1;printf("B::vfptr\n");PrintVFptr((Vfptr*)*(int*)(&t1));printf("A::vfptr\n");PrintVFptr((Vfptr*)*(int*)(&t2));}

在这里插入图片描述
在这里插入图片描述
再解读前,我们先来说明一个东西,只要是该类的虚函数,就会被存入该类的虚函数表中,并且对于单继承来说,每个类只有一份虚函数表,子类继承了父类的虚函数表,并且将重写的虚函数覆盖为自己的。也就是说子类的虚函数表是:继承父类的并进行重写覆盖后+自己的虚函数。

我们回来来解读上结果:Func1,Func4都进行了重写,所以我们发现其A与B打印出的函数地址不同。而Func2是通过继承下来的虚函数,但并没有进行重写,会存在虚表中,因此其在A和B的虚表中的函数指针相同。Func3则是子类中独有的虚函数,因此只在子类的需表中有。

所以多态的原理是:在编译阶段会形成虚函数表,在调用构造函数的初始化列表阶段会对虚函数表进行初始化。当程序运行后,在指向对象的虚函数表中去找对应的虚函数,这也是为什么我们前面说指向谁就调用谁,而对于普通函数来说,其在编译阶段就已经确定了调用谁。
在这里插入图片描述

动态绑定与静态绑定

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

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

三,抽象类

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

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

我们来看一道例题:

class A{public:virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}virtual void test(){ func();}};class B : public A{public:void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }};int main(int argc ,char* argv[]){B*p = new B;p->test();return 0;}

问这道题的输出是什么?
在这里插入图片描述

答案是:B->1
这是为什么呢?
首先我们的我们用一个指向B对象的指针p去调用test函数,而test函数是父类中的虚函数,没有重写,会继承到子类B中,这时我们会忽略一个问题—this指针,test中的this指针是A类类型的指针,这符合构成多态的一个条件:父类指针或引用去调用虚函数。 我们将B*指针传过去后会发生切片,在test中调用func(),由于虚函数的继承是几口继承,因此其会继承父类的接口,用子类的实现。感觉像头和身子拼接起来的一样。因此,在这道题中会用到父类中func函数的缺省参数,而实现部分则用的是子类中的func。所以答案为:B->1。

四,多继承中的虚函数表

以以下代码为例:

class Base1
{
public:virtual void func1(){cout << "Base1::func1" << endl;}protected:int _a;};class Base2
{
public:virtual void func1(){cout << "Base2::func1" << endl;}virtual void func2(){cout << "Base2::func2" << endl;}protected:int _b;};class Boss:public Base1,public Base2
{
public:virtual void func1(){cout << "Boos::func1" << endl;}virtual void func2(){cout << "Boos::func2" << endl;}virtual void func3(){cout << "Boos::func3" << endl;}protected:int _c;};typedef void(*Vfptr)();void PrintVFptr(Vfptr* arr)
{for (int i = 0; arr[i] != nullptr; ++i){printf("vfptr[%d]->%p  ", i, arr[i]);Vfptr pf = arr[i];pf();}
}int main()
{Base1 b1;Base2 b2;Boss d;cout << "Base1" << endl;PrintVFptr((Vfptr*)*(int*)(&b1));cout << "Base2" << endl;PrintVFptr((Vfptr*)*(int*)(&b2));cout << "Boos--1" << endl;PrintVFptr((Vfptr*)*(int*)(&d));cout << "Boos--2" << endl;PrintVFptr((Vfptr*)(*(int*)((char*)&d + sizeof(Base1))));}

在这里插入图片描述
在这里插入图片描述
通过上述窗口和打印出的结果看,我们发现多继承其有n个虚函数表,n与继承的父类个数有关。并且,子类自身的虚函数放在第一个虚表中,如上述func3()。

还有一个有趣的现象:

Base1 b1;Base2 b2;Boss d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();

在这里插入图片描述
若我们用子类的地址赋值给不同的父类指针去调用func1()按理来说,其两个指针都指向同一个子类对象,并且func1()都进行了重写,因此ptr1,ptr2 , Boos虚表中的func1()的地址应该是一样的,但是结果却不一样,这是为什么呢?
下面是d对象的模型:

在这里插入图片描述
因此当ptr2去调用func1()时,其传过去的this指针,并不是d的起始地址,因此为了使其this指针变为d的this 指针,这里编译器会进行一个操作,计算出Base1的大小,将ptr2减去Base1的大小,这时this指针就指向了d的起始位置,自然调用的就是d对象中的func1()。

补充两个关键字:

  1. final:修饰虚函数,表示该虚函数不能再被重写。
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

在这里插入图片描述

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

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

相关文章

谈谈对Android音视频开发的探究

在日常生活中&#xff0c;视频类应用占据了我们越来越多的时间&#xff0c;各大公司也纷纷杀入这个战场&#xff0c;不管是抖音、快手等短视频类型&#xff0c;虎牙、斗鱼等直播类型&#xff0c;腾讯视频、爱奇艺、优酷等长视频类型&#xff0c;还是Vue、美拍等视频编辑美颜类型…

list模拟

之前模拟了string,vector&#xff0c;再到现在的list&#xff0c;list的迭代器封装最让我影响深刻。本次模拟的list是双向带头节点的循环链表&#xff0c;该结构虽然看起来比较复杂&#xff0c;但是却非常有利于我们做删除节点的操作&#xff0c;结构图如下。 由于其节点结构特…

【etcd】docker 启动单点 etcd

etcd: v3.5.9 etcd-browser: rustyx/etcdv3-browser:latest 本文档主要描述用 docker 部署单点的 etcd&#xff0c; 用 etcd-browser 来查看注册到 etcd 的 key 默认配置启动 docker run -d --name ai-etcd --networkhost --restart always \-v $PWD/etcd.conf.yml:/opt/bitn…

从gRPC入门到放弃

文章目录 gRPCgRPC是什么为什么要用gRPC安装gRPC安装gRPC安装Protocol Buffers v3安装插件检查 gRPC的开发方式编写.proto文件定义服务生成指定语言的代码编写业务逻辑代码 gRPC入门示例编写proto代码编写Server端Go代码编写Client端Go代码gRPC跨语言调用生成Python代码编写Pyt…

K8s安全配置:CIS基准与kube-bench工具

01、概述 K8s集群往往会因为配置不当导致存在入侵风险&#xff0c;如K8S组件的未授权访问、容器逃逸和横向攻击等。为了保护K8s集群的安全&#xff0c;我们必须仔细检查安全配置。 CIS Kubernetes基准提供了集群安全配置的最佳实践&#xff0c;主要聚焦在两个方面&#xff1a;主…

基于双层优化的微电网系统规划设计方法(Matlab代码实现)

目录 &#x1f4a5;1 概述 1.1 微电网系统结构 1.2 微电网系统双层规划设计结构 1.3 双层优化模型 1.4 上层容量优化模型 1.5 下层调度优化模型 &#x1f4da;2 运行结果 &#x1f389;3 文献来源 &#x1f308;4 Matlab代码、数据、文章讲解 &#x1f4a5;1 概述 文献来源&…

牛客网Verilog刷题——VL51

牛客网Verilog刷题——VL51 题目答案 题目 请编写一个十六进制计数器模块&#xff0c;计数器输出信号递增每次到达0&#xff0c;给出指示信号zero&#xff0c;当置位信号set 有效时&#xff0c;将当前输出置为输入的数值set_num。模块的接口信号图如下&#xff1a; 模块的时序图…

作者推荐 | 【底层服务/编程功底系列】「底层技术原理」史上最清晰的采用程序员的视角方式进行深入探索Linux零拷贝技术原理及实现

采用程序员的视角方式进行深入探索Linux零拷贝技术原理及实现 背景介绍什么是零拷贝第一步&#xff1a;用户空间数据复制到内核空间第二步&#xff1a;用户空间数据复制到内核空间第三步&#xff1a;用户空间数据再次复制到内核空间第四步&#xff1a;内核态数据buffer写回到So…

html5播放器视频切换和连续播放的实例

当前播放器实例可以使用changeVid接口切换正在播放的视频。当有多个视频&#xff0c;在上一个视频播放完毕时&#xff0c;自动播放下一个视频时也可采用该处理方式。 const option {vid: 88083abbf5bcf1356e05d39666be527a_8,//autoplay: true,//playsafe: , //PC端播放加密视…

超详细|ChatGPT论文润色教程

本文讲述使用中科大开源ChatGPT论文辅助工具&#xff0c;对论文进行润色 祝看到本教程的小伙伴们都完成论文&#xff0c;顺利毕业。 可以加QQ群交流&#xff0c;一群&#xff1a; 123589938 第一章 介绍 今天给大家分享一款非常不错的ChatGPT论文辅助工具&#xff0c;使用了专…

电脑更新win10黑屏解决方法

电脑更新win10黑屏解决方法 电脑黑屏出现原因解决步骤 彻底解决 电脑黑屏 出现原因 系统未更新成功就关机&#xff0c;导致系统出故障无法关机 解决步骤 首先长安电源键10s关机 按电源键开机&#xff0c;出现logo时按F8进入安全模式。 进入自动修复环境后&#xff0c;单击…

ElasticSearch 7.x

前言 elastic表示可伸缩&#xff0c;search表示查询。所以es的核心即为查询。通常情况下&#xff0c;我们的数据可以分为三类&#xff1a;结构化数据、非结构化数据、半结构化数据。 结构化数据&#xff1a;一般会用特定的结构来组织和管理数据&#xff0c;表现为二维表结构。…

Spring Bean的生命周期

文章目录 Spring Bean的生命周期加载Bean对象创建Bean对象构造对象填充属性初始化实例注册销毁 销毁 Spring Bean的生命周期 Spring Bean的生命周期就是指Bean对象从创建到销毁的过程&#xff0c;大体可以分为&#xff1a;实例化、属性赋值、初始化、使用、销毁。 加载Bean对象…

【数据分析】numpy (二)

numpy作为数据分析&#xff0c;深度学习常用的库&#xff0c;本篇博客我们来介绍numpy的一些进阶用法&#xff1a; 一&#xff0c;numpy的常用简单内置函数&#xff1a; 1.1求和&#xff1a; a np.array([[1, 2],[3, 4]]) np.sum(a)10 1.2求平均值&#xff1a; np.mean(a…

向 Maven 中央仓库上传一个修改过的基于jeecg的autoPOI的 jar包记录

1、注册https://issues.sonatype.org/账号 下面就代表注册好了&#xff0c;同时提交的工单也通过了 2、这里主要是goupId 需要进行认证&#xff0c;需要到域名注册商近一个txt的解析&#xff0c;以便确保这个是你的 通过下面来验证你的域名信息&#xff0c;这里主要是上面的工…

学生信息管理系统springboot学校学籍专业数据java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 学生信息管理系统springboot 系统3权限&#xff1a;超…

如何使用fiddler进行抓包

首先需要下载fiddler&#xff0c;推荐使用bing搜索引擎搜索&#xff08;百度搜狗一般搜这种工具展示的前几个全都是广告&#xff09;&#xff0c;直接搜索fiddler&#xff0c;搜出来第一个fiddler官网 然后直接点击download下载 进入下载页面后&#xff0c;正确填写一个邮箱&a…

unity TextMeshPro 富文本

<b>粗体标签</b> <i>斜体标签</i> <u>下划线标签</u> <s>删除线标签</s> <sup>上标标签</sup>前面后边上标签 5<sup>。</sup>C <sub>下标标签&#xff0c;如&#xff1a;</sub>H<sub&…

【雕爷学编程】MicroPython动手做(33)——物联网之天气预报2

天气&#xff08;自然现象&#xff09; 是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象&#xff0c;即某瞬时内大气中各种气象要素&#xff08;如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等&#xff…

庄懂的TA笔记(十九)<特效:顶点 平移+缩放+旋转+幽灵夜巡效果)>

庄懂的TA笔记&#xff08;十九&#xff09;&#xff1c;特效&#xff1a;顶点 平移缩放旋转幽灵夜巡效果)&#xff1e; 大纲&#xff1a; 效果展示&#xff1a; 正文&#xff1a; 一、顶点平移&#xff1a; 1、代码实现&#xff1a; 1.1、声明移动范围&#xff0c;移动速度。 _…