C++?多态!!!

一、引言

        众所周知,C++有三大特性,它们分别是封装、继承和多态,在之前的文章中已经详细介绍过封装和继承了,今天我们将一起学习多态相关的知识,如果还想了解封装、继承相关的知识,可以跳转到以下链接:
       

        1、封装:C++?类和对象(上)!!!-CSDN博客

        2、继承:C++?继承!!!-CSDN博客

二、多态的概念

        1、概念

        通俗来讲,多态表示多种状态,即就是说当面对不同类型、不同特点的对象时,处理一个问题时采用不同的方式从而产生不同的效果,这就是多态

        2、分类

        事实上,多态细分之下有两种,它们分别是静态多态和动态多态,我们常说的多态事实上代指动态多态,也就是我们今天将要主要讨论的内容,在详细了解了多态的相关知识之后我们将再来理解这两个概念

        3、从实际的角度认识多态

        上面我们介绍了多态的概念,这样我们可以按图索骥,大概举几个日常生活中常见的多态的实际应用:


                (1).打滴滴

                在打滴滴时,新人用户常常会享受较大的优惠力度,小编记得在我第一次打滴滴时,价格优惠到了4元,那天的路程还挺远的,如果放在今天可能会在十元往上,这里就用到了多态的相关知识(猜测),当一个新人用户和一个老用户同样的调用"打车"接口时,却对应了不同的优惠力度,这正好对应了多态的概念

                (2).买票系统

                我们日程生活中会进行各种各样的买票操作,比如各个景点或者是买回家的车票,不难发现,常见的对象会被平台分为:普通身份、学生、军人等

                当这些对象同样调用买票接口时,普通身份会全家买票,学生是半价买票,军人常见的则是优先买票,很明显,不同的对象调用同一接口,产生了不同的效果,对应了多态的概念

        通过以上两个常见的概念,我们可以感受到多态的相关知识是存在在我们生活中的方方面面的

三、多态的定义及实现

        1、虚函数

        虚函数:即就是被virtual关键字修饰的函数:
        

class Person
{
public:virtual void buy_t(){cout << "全价购票" << endl;}
};

        2、虚函数的重写

        虚函数的重写:派生类中有一个函数跟基类的虚函数三同(即函数名、函数参数、函数返回值都相同)的函数,那么就称该派生类重写(覆盖)了基类的虚函数,例如:
        

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

        以上的情况我们就说Student类重写了Person类中的buy_t函数

        但是需要注意的是,虚函数重写存在以下两个例外:
               

                (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关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor

                所以为什么要这样特殊处理析构函数,使它可以构成虚函数重写呢?,我们从下面一个例子来看:
                

class Person {public:virtual ~Person() {cout << "~Person()" << endl;}};class Student : public Person {public:virtual ~Student() { cout << "~Student()" << endl; }};// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main(){Person* p1 = new Person;Person* p2 = new Student;
delete p1;delete p2;}return 0;

                上面的代码中,p1、p2都是Person*的变量,随后调用delete对这两个动态申请的空间进行释放,事实上delete对于自定义类型会调用对应类的析构函数,此时就产生了一个问题:两个空间都会调用Person的析构函数,这是我们不想看到的,我们希望的是对于p1调用Person的析构函数,而对于p2则是调用Student的析构函数

                这时候我们可以认真的观察一下我们上面的需求,好像就是使用基类的指针来调用同一个函数,同时我们想让该调用动作对于不同的对象产生不同的效果,是的,这就是我们前面多态讨论过的需求,现在只有一个条件还没有满足,就是函数名并不相同,所以我们顺理成章的想到要让编译器对析构函数名进行特殊处理,这样在将基类的析构函数写为虚函数时,自然的就解决了上面的问题

        3、多态的构成条件

        多态是在继承关系中,不同的类对象调用同一函数,产生了不同的行为,比如Student继承了Person,这时候Person对象全价买票,Student对象半价买票,所以首先的,多态是存在在继承关系中的

        在继承关系中要构成多态还有两个条件:
                (1).必须通过基类的指针或者饮用调用函数

                (2).被调用的函数必须是虚函数,同时派生类对基类的虚函数进行重写

        下面是构成多态的一个完整例子:

        

#include <iostream>
using namespace std;
//多态
class Person
{
public:virtual void buy_t(){cout << "全价购票" << endl;}
};
class Student : public Person
{
public:void buy_t(){cout << "半价购票" << endl;}
};
void func(Person& rp)
{rp.buy_t();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
}

        这一段代码的运行结果如下:
        

四、C++11中提供的两个相关的关键字:override和final

        经过上面的讲解,我们发现,C++中构成重写从而构成多态的过程时非常严格的,而在平常的代码工作中我们很容易会犯一些错误,比如:大小写的问题、字母顺序的问题,这些问题产生时是很难发现的,对于这些问题,只是没有构成重写,但并没有编译、链接的错误,不会报错,非常头疼,所以在C++11中我们提供了override和final两个关键字,它们两个可以帮助我们检查这一类问题

        1、final:该关键字有两个作用

                (1).修饰虚函数,被修饰的函数不能被重写:
                
class Person
{
public:virtual void buy_t  ()final//final修饰了该函数{cout << "全价购票" << endl;}
};
class Student : public Person
{
public:void buy_t()//这个位置会报错:无法重写“final”函数 "Person::buy_t"{cout << "半价购票" << endl;}
};
                (2).修饰一个类,被修饰的类不能被继承  

                

#include <iostream>
using namespace std;
//多态
class Person final//使用final修饰这个类
{
public:virtual void buy_t(){cout << "全价购票" << endl;}
};
class Student : public Person//这个位置会报错:不能将"final"类类型用作基类
{
public:void buy_t(){cout << "半价购票" << endl;}
};

        2、override:检查派生类函数是否重写了基类某个虚函数,如果没有就报错

        

class Person
{
public:virtual void buy_t(){cout << "全价购票" << endl;}
};
class Student : public Person
{
public:void buy_tx() override//override修饰该函数//该位置报错:使用override修饰的函数不能重写基类成员{cout << "半价购票" << endl;}
};

五、对比重载、重写(覆盖)、重定义(隐藏)

六、抽象类

        1、概念

        在虚函数的函数头之后加上=0,此时该函数被称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写了纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,它更能体现出接口继承

        下面的代码体现出了这种接口继承的思想:
        

#include <iostream>
using namespace std;
//多态
class Person
{
public:virtual void buy_t() = 0;};
class Student : public Person
{
public:void buy_t(){cout << "半价购票" << endl;}
};
class Teacher :public Person
{
public:void buy_t(){cout << "十倍价钱购票" << endl;}};
void func(Person& rp)
{rp.buy_t();
}
int main()
{Teacher t;Student s;func(t);func(s);return 0;
}

        下面是以上代码的执行结果:
               

        2、接口继承和实现继承

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

七、多态的原理

        1、虚函数表

                (1).引入
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};

                我们先通过打印的方式看一下这个问题的结果是多少?

                

                (2).解决问题

                可以看到,结果输出了8(这里要强调一下,小编实在x86的环境下输出的,环境或者平台改变可能会影响结果),这是为什么呢?或许含有虚函数的类对象进行了一些特殊处理?接下来我们通过调试的方法来看一下该类对象模型是怎样的:

                

                经过上面的调试窗口我们知道,原来在Base类中除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表,那么派生类中这个表放了些什么呢?我们接着往下分析

                为了符合多态的情景,我们先对上面的代码做出以下改造:
                

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base// 2.Derive中重写Func1// 3.Base再增加一个虚函数Func2和一个普通函数Func3class 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;}

                接下来我们一起观察这个加强版继承体系的类对象模型,从而说明派生类中的虚表有什么不同?

                

                可以观察到:继承之后的d对象模型中分为两个部分,分别是Base部分和自己的成员,而在Base部分中也有一个_vfptr指针,这意味着d不会生成自己的虚表指针,而是以继承的形式沿用了Base类的指针,而两个指针指向的位置是不同的,这就是说两个类的虚表是不同的,事实上的确是这样的,派生类会首先继承基类的虚表,然后对于重写过的函数将新的函数指针覆盖原本的函数指针,形成了属于自己的虚表

        2、多态的实现

        经过上面对于虚表指针和虚表的认识,我们大概也可以想到多态究竟是如何实现的

        事实上,多态的实现原理就是虚表指针存在在父子类中基类的部分,所以必须使用基类的指针或者引用调用(不能直接使用对象调用是因为对象的切片赋值会丢失信息,而指针和引用的切片赋值不会),同时通过虚表指针我们就可以找到虚表,父子类的虚表不同,找到的函数也就不同,这时候就实现了多态调用函数

        3、动态绑定与静态绑定

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

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

八、结语

        这就是本期有关多态的全部内容了,感谢大家的阅读,欢迎各位于晏、亦菲和我一起交流、学习、进步!!!                              、                
                

              

                              

                        

        

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

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

相关文章

electron安装报错处理

electron安装报错 解决方法&#xff1a; 修改 C:\Users\用户名.npmrc下配置文件 添加代码 electron_mirrorhttps://cdn.npmmirror.com/binaries/electron/ electron_builder_binaries_mirrorhttps://npmmirror.com/mirrors/electron-builder-binaries/最后代码 registryhtt…

Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速

Windows10下使用QEMU安装Ubuntu20.04虚拟机&#xff0c;并启用硬件加速 作者将狼才鲸创建日期2025-05-30 CSDN阅读地址&#xff1a;Windows10下使用QEMU安装Ubuntu20.04虚拟机&#xff0c;并启用硬件加速 本文档源码地址&#xff1a;Windows10下使用QEMU安装Ubuntu20.04虚拟机…

顶刊SCS | 基于视觉语言大模型推理分割的建筑足迹尺度功能分类, 样本数据和代码已开源!

论文介绍 题目&#xff1a;Visual-language reasoning segmentation (LARSE) of function-level building footprint across Yangtze River Economic Belt of China 期刊&#xff1a;Sustainable cities and society&#xff08;中科院一区TOP&#xff0c;IF10.5&#xff09;…

【软件】navicat 官方免费版

Navicat Premium Lite https://www.navicat.com.cn/download/navicat-premium-lite

每个路由器接口,都必须分配所属网络内的 IP 地址,用于转发数据包

在IP网络中&#xff0c;主机&#xff08;Host&#xff09;和路由器接口&#xff08;Router Interface&#xff09;都需要分配网络地址&#xff08;IP地址&#xff09;。 1. 主机&#xff08;Host&#xff09;的IP地址分配 (1) 作用 主机的IP地址用于唯一标识该设备&#xff0…

鸿蒙OSUniApp页面切换动效实战:打造流畅精致的转场体验#三方框架 #Uniapp

UniApp页面切换动效实战&#xff1a;打造流畅精致的转场体验 引言 在移动应用开发中&#xff0c;页面切换动效不仅能提升用户体验&#xff0c;还能传达应用的品质感。随着HarmonyOS的普及&#xff0c;用户对应用的动效体验要求越来越高。本文将深入探讨如何在UniApp中实现流畅…

Tesseract OCR 安装与中文+英文识别实现

一、下载 https://digi.bib.uni-mannheim.de/tesseract/ 下载&#xff0c;尽量选择时间靠前的&#xff08;识别更好些&#xff09;。符合你的运行机&#xff08;我的是windows64&#xff09; 持续点击下一步安装&#xff0c;安装你认可的路径即可&#xff0c;没必要配置环境变…

Visual Studio 2022 发布独立的 exe 文件

我们在用 Visual Studio 2022 写好一个 exe 程序之后&#xff0c;如果想把这个拿到其他地方运行&#xff0c;需要把 exe 所在的文件夹一起拿过去。 编译出来的 exe 文件需要其他几个文件一同放在同一目录才能运行&#xff0c;原因在于默认情况下&#xff0c;Visual Studio 是把…

Kotlin-特殊类型

文章目录 数据类型枚举类型匿名类和伴生对象单例类伴生对象 数据类型 声明一个数据类非常简单: //在class前面添加data关键字表示为一个数据类 data class Student(var name: String, var age: Int)数据类声明后,编译器会根据主构造函数中声明的所有属性自动为其生成以下函数…

在线博客系统【测试报告】

&#x1f552; 一. 项目背景 由于纸质笔记容易丢失&#xff0c;携带不变&#xff0c;为了方便自己学习的过程中记录笔记&#xff0c;特开发了这个博客系统。这个系统后端采用 SpringBoot MyBatis SpringMVC &#xff1b;前端使用Html CSS JS&#xff1b;数据库使用的是Mysq…

每日刷题c++

快速幂 #include <iostream> using namespace std; #define int long long int power(int a, int b, int p) {int ans 1;while (b){if (b % 2){ans * a;ans % p; // 随时取模}a * a;a % p; // 随时取模b / 2;}return ans; } signed main() {int a, b, p;cin >> a …

Python中的变量、赋值及函数的参数传递概要

Python中的变量、赋值及函数的参数传递概要 python中的变量、赋值 python中的变量不是盒子。 python中的变量无法用“变量是盒子”做解释。图说明了在 Python 中为什么不能使用盒子比喻&#xff0c;而便利贴则指出了变量的正确工作方式。 如果把变量想象为盒子&#xff0c;那…

KVM 安装 Ubuntu 22

在 KVM 中安装 Ubuntu 22 虚拟机。 首先创建硬盘文件 sudo qemu-img create -f qcow2 /app/vms/ubuntu22.qcow2 100G安装Ubuntu 22 sudo virt-install \--name ubuntu22 \--ram 4096 \--vcpus 2 \--disk path/app/vms/ubuntu22.qcow2,formatqcow2 \--os-type linux \--os-va…

基于生产-消费模式,使用Channel进行文件传输(Tcp方式)

Client端&#xff1a; #region 多文件传输 public class FileMetadata {public string FileName { get; set; }public long FileSize { get; set; } }class Program {const int PORT 8888;const int BUFFER_SIZE 60 * 1024 * 1024;//15s-50 25s-64 33s-32 27s-50 31s-40 25…

【后端高阶面经:Elasticsearch篇】39、Elasticsearch 查询性能优化:分页、冷热分离与 JVM 调优

一、索引设计优化:构建高效查询的基石 (一)分片与副本的黄金配置 1. 分片数量计算模型 # 分片数计算公式(单分片建议30-50GB) def calculate_shards(total_data_gb, single_shard_gb=30):return max

学习路之PHP--easyswoole3.3安装入门

学习路之PHP--easyswoole安装入门 一、安装swoole扩展二、安装easyswoole三、指定PHP版本安装四、启动swoole五、EasySwoole的入门学习如果报&#xff1a;not controller class match 六、学习推荐&#xff1a; 0、centos 7、php7.2.33、easyswoole 3.3 一、安装swoole扩展 二、…

Ad Hoc

什么是 Ad Hoc&#xff1f; Ad hoc 一词源于拉丁语&#xff0c;意为“为此目的”或“为此特定原因”。一般来讲&#xff0c;它指的是为解决某一特定问题或任务&#xff08;而非为了广泛重复应用&#xff09;而设计的行动、解决方案或组合。在加密货币和区块链领域&#xff0c;…

Lines of Thought in Large Language Models

Lines of Thought in Large Language Models 《Lines of Thought in Large Language Models》(大语言模型中的思维链)聚焦于分析大语言模型(LLMs)在生成文本时,其内部向量轨迹的统计特性。 核心目标是揭示LLMs复杂的“思维过程”(即文本生成时的隐藏状态变化)能否被简…

npm/yarn/pnpm安装时Sharp模块报错解决方法

在安装依赖模块时&#xff0c;npm/yarn/pnpm安装时Sharp模块报错解决方法。 打开源代码发现&#xff1a;使用的下载地址是github地址&#xff0c;就是因为国内经常无法访问github造成的。 解决办法&#xff1a; 把涉及到的下载包设置不要从github上下载&#xff0c;设置成淘宝…

基于CEEMDAN-Transformer-BiLSTM的多特征风速气候预测的完整实现方案及PyTorch源码解析

基于CEEMDAN-Transformer-BiLSTM的多特征风速气候预测的完整实现方案及PyTorch源码解析 一、模型架构设计 1.1 整体框架 该模型采用三级架构设计&#xff08;图1&#xff09;&#xff1a; CEEMDAN分解层&#xff1a;对非平稳风速序列进行自适应分解多模态特征融合模块&#…