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.xdnf.cn/news/1554837.html

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

相关文章

C语言 | Leetcode C语言题解之第457题环形数组是否存在循环

题目&#xff1a; 题解&#xff1a; int next(int* nums, int numsSize, int cur) {return ((cur nums[cur]) % numsSize numsSize) % numsSize; // 保证返回值在 [0,n) 中 }bool circularArrayLoop(int* nums, int numsSize) {for (int i 0; i < numsSize; i) {if (!n…

C++ | Leetcode C++题解之第456题132模式

题目&#xff1a; 题解&#xff1a; class Solution { public:bool find132pattern(vector<int>& nums) {int n nums.size();vector<int> candidate_i {nums[0]};vector<int> candidate_j {nums[0]};for (int k 1; k < n; k) {auto it_i upper_…

Ubuntu24.04远程开机

近来在几台机器上鼓捣linux桌面&#xff0c;顺便研究一下远程唤醒主机。 本篇介绍Ubuntu系统的远程唤醒&#xff0c;Windows系统的唤醒可搜索相关资料。 依赖 有远程唤醒功能的路由器&#xff08;当前一般都带这个功能&#xff09;有线连接主机&#xff08;无线连接有兴趣朋友…

信息安全工程师(33)访问控制概述

前言 访问控制是信息安全领域中至关重要的一个环节&#xff0c;它提供了一套方法&#xff0c;旨在限制用户对某些信息项或资源的访问权限&#xff0c;从而保护系统和数据的安全。 一、定义与目的 定义&#xff1a;访问控制是给出一套方法&#xff0c;将系统中的所有功能和数据…

【JAVA开源】基于Vue和SpringBoot的宠物咖啡馆平台

本文项目编号 T 064 &#xff0c;文末自助获取源码 \color{red}{T064&#xff0c;文末自助获取源码} T064&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

仿RabbitMQ实现消息队列三种主题的调试及源码

文章目录 开源仓库和项目上线广播交换模式下的测试直接交换模式下的测试主题交换模式下的测试 开源仓库和项目上线 本项目已开源到下面链接下的仓库当中 仿RabbitMQ实现消息队列 广播交换模式下的测试 消费者客户端 在进行不同测试下&#xff0c;消费者客户端只需要改变交换机…

基于SpringBoot+Vue+MySQL的中医院问诊系统

系统展示 用户前台界面 管理员后台界面 医生后台界面 系统背景 随着信息技术的迅猛发展和医疗服务需求的不断增加&#xff0c;传统的中医院问诊流程已经无法满足患者和医院的需求。纸质病历不仅占用大量存储空间&#xff0c;而且容易丢失和损坏&#xff0c;同时难以实现信息的快…

Acwing 背包问题

背包问题 首先&#xff0c;什么是背包问题&#xff1f; 给定N个物品和一个容量为V的背包&#xff0c;每个物品有体积和价值两种属性&#xff0c;在一些限制条件下&#xff0c;将一些物品放入背包&#xff0c;使得在不超过背包体积的情况下&#xff0c;能够得到的最大价值。根据…

【redis学习篇1】redis基本常用命令

目录 redis存储数据的模式 常用基本命令 一、set 二、keys pattern keys 字符串当中携带问号 keys 字符串当中携带*号 keys 【^字母】 keys * 三、exists 四、del 五、expire 5.1 ttl命令 5.2key删除策略 5.2.1惰性删除 5.2.2定期删除 六、type key的数据类型…

Windows安全加固详解

一、补丁管理 使用适当的命令或工具&#xff0c;检查系统中是否有未安装的更新补丁。 Systeminfo 尝试手动安装一个系统更新补丁。 • 下载适当的补丁文件。 • 打开命令提示符或PowerShell&#xff0c;并运行 wusa.exe <patch_file_name>.msu。 二、账号管…

Pikachu-Sql-Inject - 暴力破解

之前的破解&#xff0c;一般都需要 information_schema.schemata 、 information_schema.tables 、information_schema.columns 的权限&#xff0c;如果没有权限&#xff0c;就需要暴力破解&#xff1b; 如构造payload ,这个 abc 表就是我们要确定是否存在的表 vince and ex…

GPTQ vs AWQ vs GGUF(GGML) 速览和 GGUF 文件命名规范

简单介绍一下四者的区别。 参考链接&#xff1a;GPTQ - 2210.17323 | AWQ - 2306.00978 | GGML | GGUF - docs | What is GGUF and GGML? 文章目录 GPTQ vs AWQ vs GGUF&#xff08;GGML&#xff09; 速览GGUF 文件命名GGUF 文件结构文件名解析答案 附录GGUF 文件命名GGUF 文件…

Pandas基础学习

导入 导入pandas一般是这样导入的 import pandas as pdSeries 创建 s1 pd.Series([5, 17, 3, 26, 31])注意Series的第一个字母要大写&#xff0c;表明这其实是Series类的构建函数, 返回的是Series类的实例 获得元素或者索引 单独获得元素 s1.values单独获得索引值 s…

Flink 03 | 数据流基本操作

Flink数据流结构 DataStream 转换 通常我们需要分析的业务数据可能存在如下问题&#xff1a; 数据中包含一些我们不需要的数据 数据格式不方面分析 因此我们需要对原始数据流进行加工&#xff0c;比如过滤、转换等操作才可以进行数据分析。 “ Flink DataStream 转换主要作…

Kubernetes-环境篇-01-mac开发环境搭建

1、brew安装 参考知乎文章&#xff1a;https://zhuanlan.zhihu.com/p/111014448 苹果电脑 常规安装脚本&#xff08;推荐 完全体 几分钟安装完成&#xff09; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"苹果电脑 极…

【Android】中级控件

其他布局 相对布局RelativeLayout RelativeLayout下级视图的位置是相对位置&#xff0c;得有具体的参照物才能确定最终位置。如果不设定下级视图的参照物&#xff0c;那么下级视图默认显示在RelativeLayout内部的左上角。用于确定视图位置的参照物分两种&#xff0c;一种是与…

自动驾驶系列—全面解析自动驾驶线控制动技术:智能驾驶的关键执行器

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

JavaScript-上篇

JS 入门 JS概述 JavaScript&#xff08;简称JS&#xff09;是一种高层次、解释型的编程语言&#xff0c;最初由布兰登艾奇&#xff08;Brendan Eich&#xff09;于1995年创建&#xff0c;并首次出现在网景浏览器中。JS的设计初衷是为Web页面提供动态交互功能&#xff…

Leetcode - 140双周赛

目录 一&#xff0c;3300. 替换为数位和以后的最小元素 二&#xff0c;3301. 高度互不相同的最大塔高和 三&#xff0c;3302. 字典序最小的合法序列 四&#xff0c;3303. 第一个几乎相等子字符串的下标 一&#xff0c;3300. 替换为数位和以后的最小元素 本题直接暴力求解&a…

【hot100-java】【将有序数组转换为二叉搜索树】

二叉树篇 BST树 递归直接实现。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNo…