C++之多态篇(超详细版)

1.多态概念

多态就是多种形态,表示去完成某个行为时,当不同的人去完成时会有不同的形态,举个例子在车站买票,可以分为学生票,普通票,军人票,每种票的价格是不一样的,当你是不同的身份时去车站买票,就需要交不同的价钱,这个就是表示多态的行为。
在这里插入图片描述

2.多态的定义

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student :public Person
{
public:virtual void BuyTicket(){cout << "买票半价" << endl;}
};void Func(Person& people)
{people.BuyTicket();
}void test()
{Person Mike;Func(Mike);Student s;Func(s);
}
int main()
{test();return 0;
}

上面的代码就是简单的多态定义,对于初学者看到上面的代码可能会一脸懵,别着急,容我细细为你们分析!
在这里插入图片描述

3.虚函数的重写

在上面我们提到了虚函数,解释了什么是虚函数,那么如何重写虚函数呢?

(1)重写虚函数(也叫覆盖)是派生类中重写出一个和基类的虚函数完全相同的虚函数,什么是完全相同呢?(派生类的虚函数和基类的虚函数的返回类型和函数名和参数列表都相同).

在这里插入图片描述
是不是派生类中虚函数也有virtual关键字,如果我们把它去掉可以吗?
我们看看下面代码

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student :public Person
{
public:void BuyTicket(){cout << "买票半价" << endl;}
};void Func(Person& people)
{people.BuyTicket();
}void test()
{Person Mike;Func(Mike);Student s;Func(s);
}
int main()
{test();return 0;
}

在这里插入图片描述
这里有老铁就会疑问了,为什么派生类虚函数可以没有virtual关键字呢?我们来调试一下代码吧
在这里插入图片描述
我们发现派生类继承下来了基类的虚函数,所以派生类也保持着虚函数的属性,所以程序没有问题,虽然程序没问题,但是这种写法不规范,不建议使用。

虚函数重写的两个特殊情况

1.协变:基类虚函数和派生类虚函数的返回值类型不同(基类返回的是基类对象的指针/引用;派生类返回的是派生类对象的指针/引用)

class A {};
class B : public A {};
class Person {
public:virtual A* f() { return new A; }
};
class Student : public Person {
public:virtual B* f() { return new B; }
};

2.虚构函数的重写(基类和派生类的函数名不同)

如果基类虚函数是析构函数,那么派生类的虚构函数无论有没有virtual关键字都会对基类析构函数构成重写。

class A {
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A 
{
public:~B(){cout << "~B()" << endl;}
};int main()
{A* p1 = new A;B* p2 = new B;delete p1;delete p2;return 0;
}

在这里插入图片描述
代码完全没问题
如果派生类和基类析构函数的虚函数的函数名不同会也能构成重写。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

C++override和final关键字

看完上面的文章,我们知道C++的对函数的重写要求很严格,但在某些时候我们可能会出现写错函数名从而导致函数不能进行重载,这个错误编译阶段是不会报错的,所以如果我们debug就很难受了。所以C++11提供了override和final关键字来帮助我们检查是否完成重写。

我们来看看出现基类的虚函数和派生类的虚函数的函数名不同,看编译器会不会在编译阶段报错

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student :public Person
{
public:virtual void BuyTickte(){cout << "买票半价" << endl;}
};void Func(Person& people)
{people.BuyTicket();
}void test()
{Person Mike;Func(Mike);Student s;Func(s);
}
int main()
{test();return 0;
}

在这里插入图片描述
编译阶段没有任何问题,我们再来看看运行结果,结果应该是买票全价和买票半价

###在这里插入图片描述结果出错了,在我们不知情的情况下去debug就很难查找出原因了。
我们再加上override关键字试试

class Person
{
public:virtual void BuyTicket(){cout << "买票全价" << endl;}
};class Student :public Person
{
public:virtual void BuyTickte() override{cout << "买票半价" << endl;}
};void Func(Person& people)
{people.BuyTicket();
}void test()
{Person Mike;Func(Mike);Student s;Func(s);
}
int main()
{test();return 0;
}

我们再来看看编译结果
在这里插入图片描述
直接就报错没有重写基类,所以证明了override关键字可以帮助我们检查派生类是否和基类构成重写。

我们明白了override关键字的作用,那么final关键字作用是什么呢?

final关键字修饰虚函数,表示该虚函数不能再被重写了。

class Person
{
public:virtual void BuyTicket() final{cout << "买票全价" << endl;}
};class Student :public Person
{
public:virtual void BuyTicket() {cout << "买票半价" << endl;}
};

看看编译结果
在这里插入图片描述

重载/重写/重定义三个概念进行对比

在这里插入图片描述

4.抽象类

在虚函数后面写上=0,就表示纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象

//抽象类
class Person
{
public:x//纯虚函数virtual void BuyTicket()=0{cout << "买票全价" << endl;}
};
int main()
{Person Mike;return 0;
}

在这里插入图片描述
如果要实例化就直接报错了
那我们看看派生类继承了抽象类会怎么样?

//抽象类
class Person
{
public://纯虚函数virtual void BuyTicket()=0{cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void Ticket() {cout << "买票半价" << endl;}
};
int main()
{Student s;return 0;
}

在这里插入图片描述
由此我们可知,就算我们派生类继承了抽象类也不能实例化出对象,只有重写基类的虚函数,派生类才能实例化出对象。

//抽象类
class Person
{
public://纯虚函数virtual void BuyTicket()=0{cout << "买票全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() {cout << "买票半价" << endl;}
};
int main()
{Student s;return 0;
}

在这里插入图片描述
代码没有任何问题。

接口继承和实例继承的区别

我们知道虚函数继承是接口继承,那什么是接口继承呢?接口继承是一个类从另一个类那里继承行为规范,但并不继承具体实现,接口继承就是一个契约,它规定了某个对象能做什么,但并没有规定要怎么做。
举个例子:假设你开了一个酒店,然后需要在酒店门口设置前台,为了确保前台能够为用户提供一致的服务体验,你创建了一个行为规范指南(这个就是接口)里面列出了前台服务员必须要给用户提供的服务体验。这个行为规范指南 就是一个接口,任何想要任职你酒店的前台就必须能够提供这些服务。

普通函数是一个实现继承,派生类继承的是基类的函数的实现。

5.多态的原理

看一下下面代码结果是什么。(Win32平台)

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{cout << sizeof(Base) << endl;return 0;
}

在这里插入图片描述
为什么是8字节呢?有老铁就疑惑了,不应该是4字节吗?那就和我一起来探索一下吧。
我们调试一下吧!
在这里插入图片描述
我们发现还有一个_vfptr指针,这个指针是干啥的呢?
这个_vfptr指针叫虚函数表指针,每一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址需要放到虚函数表中,虚函数表也叫虚表。

我们调试下面的代码看看

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

在这里插入图片描述
在这里插入图片描述
通过上面的代码,我们知道每一个虚函数都在虚函数表中存在一个指针,指向这个虚函数,普通函数在虚表中没有指向自己的指针;在虚表里面的指针可以分为两部分,一部分是从基类继承下来的虚函数,如果在派生类重写基类虚函数,就会把派生类对象的虚表里面的指针给覆盖掉,生成新的指针。,另一部分是派生类自己的虚函数。

我们通过调试窗口可以看到_vfptr虚表是不是一个存放指针的数组,一般这个数组后面都会以nullptr为结尾,

那么虚函数存放在哪呢?虚函数表又存放在哪里呢?
虚函数是和普通函数一样存放在代码段中,虚函数表中存放的是指向虚函数的指针,并不是虚函数本身,vs下的虚函数表是存放在代码段中。

我们来认证一下,看看vs编译器下虚函数表是不是存放在代码段中。

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}
};int main()
{Base s;printf("虚函数表的地址:%p\n", *(int*)&s);//只取前四个字节的地址static int a = 0;printf("静态区地址:%p\n", &a);const char* ch = "hello";printf("常量区:%p\n", ch);
}

在这里插入图片描述
这证明了虚函数表在常量区中

下面我们将通过画图来理解多态工作的原理
我们以下面的代码为例

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

在这里插入图片描述
在这里插入图片描述

那么满足多态的函数调用是在编译阶段还是在运行阶段呢?
答案是运行阶段(但是虚表是在编译阶段就生成了),如果不满足多态的函数调用则是在编译阶段就调用对应的函数了。

动态绑定和静态绑定

动态绑定(后期绑定):在运行阶段,根据拿到的具体类型去确定程序的具体行为,调具体函数。
静态绑定:在编译阶段确定了程序行为(例如函数的重载)

单继承和多继承的虚函数表

我们知道派生类可以对基类进行单继承,也可以对基类进行多继承,那么两种继承方式的虚函数表有什么不同呢?

class Base 
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};int main()
{Base b;Derive d;return 0;
}

我们来调试这段代码看看单继承的虚函数表
在这里插入图片描述
我们可以看到d对象继承了基类的虚函数,并重写了func1()函数,但是在d对象中应该还有func3和func4虚函数在虚表中,这里由于编译器隐藏起来了,所以我们看不到。

我们再来看看多继承的虚函数表

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;
};int main()
{Base1 b1;Base2 b2;Derive d;return 0;
}

多继承的派生类的未重写的虚函数放在第一个继承基类部分虚函数表中
在这里插入图片描述

总结:

多态的概念比较晦涩难懂,希望各位老铁看完这篇文章能对多态有着清晰的理解!

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

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

相关文章

如何高效删除 MySQL 日志表中的历史数据?实战指南

在处理高并发的物联网平台或者其他日志密集型应用时&#xff0c;数据库中的日志表往往会迅速增长&#xff0c;数据量庞大到数百GB甚至更高&#xff0c;严重影响数据库性能。如何有效管理这些庞大的日志数据&#xff0c;特别是在不影响在线业务的情况下&#xff0c;成为了一项技…

使用Windows远程桌面连接Linux

要在Kali Linux上使用Windows远程桌面连接&#xff08;MSTSC.exe&#xff09;&#xff0c;你可以通过配置xrdp服务来实现。以下是在Kali Linux上设置xrdp以便Windows远程桌面连接的具体步骤&#xff1a; 一、安装xrdp和Xfce桌面环境 更新软件包列表&#xff1a; 打开终端&…

Python和C++混淆矩阵地理学医学物理学视觉语言模型和算法模型评估工具

&#x1f3af;要点 优化损失函数评估指标海岸线检测算法评估遥感视觉表征和文本增强乳腺癌预测模型算法液体中闪烁光和切伦科夫光分离多标签分类任务性能评估有向无环图、多路径标记和非强制叶节点预测二元分类评估特征归因可信性评估马修斯相关系数对比其他准确度 Python桑…

基于C++和Python的进程线程CPU使用率监控工具

文章目录 0. 概述1. 数据可视化示例2. 设计思路2.1 系统架构2.2 设计优势 3. 流程图3.1 C录制程序3.2 Python解析脚本 4. 数据结构说明4.1 CpuUsageData 结构体 5. C录制代码解析5.1 主要模块5.2 关键函数5.2.1 CpuUsageMonitor::Run()5.2.2 CpuUsageMonitor::ComputeCpuUsage(…

大数据技术:Hadoop、Spark与Flink的框架演进

大数据技术&#xff0c;特别是Hadoop、Spark与Flink的框架演进&#xff0c;是过去二十年中信息技术领域最引人注目的发展之一。这些技术不仅改变了数据处理的方式&#xff0c;而且还推动了对数据驱动决策和智能化的需求。在大数据处理领域&#xff0c;选择合适的大数据平台是确…

有些硬盘录像机接入视频汇聚平台EasyCVR后通道不显示/显示不全,该如何处理?

EasyCVR视频监控汇聚管理平台是一款针对大中型项目设计的跨区域网络化视频监控集中管理平台。该平台不仅具备视频资源管理、设备管理、用户管理、运维管理和安全管理等功能&#xff0c;还支持多种主流标准协议&#xff0c;如GB28181、RTSP/Onvif、RTMP、部标JT808、GA/T 1400协…

排序算法剖析

文章目录 排序算法浅谈参考资料评价指标可视化工具概览 插入排序折半插入排序希尔排序冒泡排序快速排序简单选择排序堆排序归并排序基数排序 排序算法浅谈 参考资料 数据结构与算法 评价指标 稳定性&#xff1a;两个相同的关键字排序过后相对位置不发生变化时间复杂度空间复…

数据挖掘-padans初步使用

目录标题 Jupyter Notebook安装启动 Pandas快速入门查看数据验证数据建立索引数据选取⚠️注意&#xff1a;排序分组聚合数据转换增加列绘图line 或 **&#xff08;默认&#xff09;&#xff1a;绘制折线图。bar&#xff1a;绘制条形图。barh&#xff1a;绘制水平条形图。hist&…

QT将QBytearray的data()指针赋值给结构体指针变量后数据不正确的问题

1、问题代码 #include <QCoreApplication>#pragma pack(push, 1) typedef struct {int a; // 4字节float b; // 4字节char c; // 1字节int *d; // 8字节 }testStruct; #pragma pack(pop)#include <QByteArray> #include <QDebug>int main() {testStruct …

leetcode练习 路径总和II

给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum 22 输出&a…

告别传统互动:AI数字人正全面进入人类社会

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 今天我们来聊聊AI数字人&#xff01; 当我…

全网最详细kubernetes中的资源

1、资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管理kubernetes。 kubernetes的本质上就是一个集群系统&#xff0c;用户可以在集群中部署各种服务。 所谓的部署服务&#xff0c;其实就是在kubernetes集群中运行一个个的…

C语言基础(7)之操作符(1)(详解)

目录 1. 各种操作符介绍 1.1 操作符汇总表 2. 移位操作符 2.1 移位操作符知识拓展 —— 原码、反码、补码 2.2 移位操作符讲解 2.2.1 右移操作符 ( >> ) 2.2.2 左移操作符 ( << ) 3. 位操作符 3.1 & (按位与) 3.2 | (按位或) 3.3 ^ (按位异或) 3.4…

gm/ID设计方法学习笔记(一)

前言&#xff1a;为什么需要gm/id &#xff08;一&#xff09;主流设计方法往往侧重于强反型区&#xff08;过驱>0.2V&#xff09;&#xff0c;低功耗设计则侧重于弱反型区&#xff08;<0&#xff09;&#xff0c;但现在缺乏对中反型区的简单和准确的手算模型。 1.对于…

10月2日笔记(内网资源探测篇)

内网资源探测 在内网渗透中&#xff0c;测试人员往往需要通过各种内网扫描技术来探测内网资源的情况&#xff0c;为后续的横向渗透做准备&#xff0c;通常需要发现内网存活的主机&#xff0c;并探测主机的操作系统、主机开放了哪些端口、端口上运行了哪些服务、服务的当前版本…

SQL SERVER 从嫌弃存储到爱上存储过程我给存储过程开发了版本控制工具和远程调试功能...

优缺点 SQL SERVER 爱上存储过程我给存储过程开发了版本控制工具和远程调试功能 先说说 存储过程的优缺点吧存储过程的优点 提高执行效率&#xff1a;存储过程是预编译的&#xff0c;执行速度较快&#xff0c;减少了网络传输量。 减少开发工作量&#xff1a;存储过程可以将复杂…

数据结构——对顺序线性表的操作

大家好啊&#xff0c;今天给大家分享一下对于顺序线性表的相关操作&#xff0c;跟随我的步伐来开始这一篇的学习吧&#xff01; 如果有知识性错误&#xff0c;欢迎各位指正&#xff01;&#xff01;一起加油&#xff01;&#xff01; 创作不易&#xff0c;希望大家多多支持哦…

中级软件设计师:一文搞懂下午第一题——数据流图技术

中级软件设计师&#xff1a;一文搞懂下午第一题——数据流图技术 0. 答题技巧-【11-12分】分必拿方法0. 本章的考点1. 数据流图的基本元素1.1 数据流1.2 加工1.3 数据存储(文件)1.4 外部实体1.5 分层数据流图&#xff08;DFD&#xff09;1.6 自顶至下&#xff0c;逐步精化1.6.1…

8645 归并排序(非递归算法)

### 思路 非递归归并排序通过逐步合并相邻的子数组来实现排序。每次合并后输出当前排序结果。 ### 伪代码 1. 读取输入的待排序关键字个数n。 2. 读取n个待排序关键字并存储在数组中。 3. 对数组进行非递归归并排序&#xff1a; - 初始化子数组的大小curr_size为1。 - 逐…

GO网络编程(二):客户端与服务端通信【重要】

本节是新知识&#xff0c;偏应用&#xff0c;需要反复练习才能掌握。 目录 1.C/S通信示意图2.服务端通信3.客户端通信4.通信测试5.进阶练习&#xff1a;客户端之间通信 1.C/S通信示意图 客户端与服务端通信的模式也称作C/S模式&#xff0c;流程图如下 其中P是协程调度器。可…