当前位置: 首页 > news >正文

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

八、结语

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

              

                              

                        

        

相关文章:

  • (独家)SAP VC物料 超级BOM怎么开单?怎么计算或发布表标准成本?
  • 题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转
  • Scratch节日 | 龙舟比赛 | 端午节
  • 修改Cinnamon主题
  • 【免费的高清录屏软件】OBS Studio
  • Go语言defer关键字:延迟执行的精妙设计
  • c++流之sstream/堆or优先队列的应用[1]
  • 拉普拉斯噪声
  • 易路 iBuilder:解构企业 AI 落地困境,重构智能体时代生产力范式
  • Shopify 主题开发:页脚信息架构搭建技巧
  • 包管理工具
  • #13 学习反馈
  • SpringBoot+vue+SSE+Nginx实现消息实时推送
  • 小程序跳转H5或者其他小程序
  • 低秩矩阵、奇异值矩阵和正交矩阵
  • Opencv4 c++ 自用笔记 03 滑动条、相机与视频操作
  • Spring Boot深度解析:自动配置、Starter依赖与MyBatis Plus集成指南
  • 【iptables防火墙】-- URL过滤 (Hexstring、IP、DoT和DoH)
  • Spring Boot微服务架构(九):设计哲学是什么?
  • VScode ios 模拟器安装cocoapods
  • 做外贸有哪些免费的网站有哪些/网络营销推广外包平台
  • 上蔡县做彩票网站/关键词优化推广策略
  • 卖汽车怎么做网站/社交媒体营销
  • 域名跟网站的区别吗/网络营销推广策划方案
  • 网站结构分析怎么写/长沙网站seo分析
  • 东阳做网站/seo建站是什么意思