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

【C++】多态(多态的原理)

在本篇博客中,作者将会带领你深入理解C++中的多态。


声明!!!本代码以及讲解都是在32位机器下进行完成的,64位机器下会有所不同,但大同小异。 

一.多态的概念 

什么是多态?

多态就是不同的对象做相同的事情,会有不同的结果

例如:对于去火车站买票这件事,普通人和学生去买票会有不同的结果,普通人要买全价票,而学生可以打75折。

这就是多态。

二.多态的定义及实现 

那么知道了什么是多态,现在就来讲解一下多态是如何定义和实现的。

首先我们直接来看一段构成多态的代码。

1.多态的构成条件

多态是在继承关系中,不同的类对象去调用相同的函数时,出现不同的结果。

那么如何才能构成多态呢?

1.必须要用基类的指针或者引用调用函数

2.所调用的函数必须是虚函数,且派生类必须对虚函数进行重写(覆盖)

2.虚函数

在成员函数前面加上virtual关键字修饰的函数就是虚函数,注意只有成员函数才能被修饰成虚函数普通的函数不能被修饰成虚函数。 

class Person
{
public://虚函数virtual void BuyTicket(){cout << "普通人:买票-全价" << endl;}
};

3.虚函数重写

虚函数重写(覆盖),即在派生类中,有一个函数与基类的函数完成相同返回类型相同,函数名相同,参数列表相同)。 

class Person
{
public://父类虚函数virtual void BuyTicket(){cout << "普通人:买票-全价" << endl;}
};class Student :public Person
{
public://对继承下来的父类虚函数下进行重写virtual void BuyTicket(){cout << "学  生:买票-半价" << endl;}
};

同时在派生类重写虚函数时,可以不加上前面的virtual关键字,因为在派生类中,有一个继承父类下来的虚函数,不加virtual也可以,但是不建议。 

class Person
{
public:virtual void BuyTicket(){cout << "普通人:买票-全价" << endl;}
};class Student :public Person
{
public://不要virtual关键字也可以,但是不建议void BuyTicket(){cout << "学  生:买票-半价" << endl;}
};
①虚函数重写的例外 

同时在这里补充,虚函数重写的两个例外。 

协变 

协变就是虚函数的返回类型可以不同基类的返回值派生类的返回值构成继承关系, 返回的是指针或者引用就能构成虚函数重写。

光说很难说清,所以看下图。

 析构函数

当将基类和派生类的析构函数修饰成虚函数时,即使析构函数的函数名不同,也能构成重写

这里虽然函数名不同,但是还是能实现重写进行多态行为,那是因为,析构函数的函数名经过编译后,都会被处理成~destructor 

 4.override以及final

接下来我们讲一下两个关键字:override、final

final 

被final修饰的虚函数不能被重写 

同时在这里补充:被final修饰的类不能被继承。 

override 

override用来修饰派生类的虚函数,被修饰的虚函数可以检查是否重写基类的虚函数而来,如果没有则会报错

用处:我们会发现虚函数的重写非常的严格,因为返回值类型,函数名,参数列表都相同才能构成重写,所以在编写代码时,可能会发生明明我想重写这个虚函数,但是因为打错函数名了,而没有造成重写,同时也没有报错等任何问题,但是在我们要重写的虚函数后面加上override后,即可进行检查被override修饰的函数是否重写于基类的虚函数。 

5.重载、重定义、重写 

看到这里,可能会有同学以及有点分不清了,因为多态有重写,继承有重定义,同时函数又有重载,初学者可能很容易搞混。所以在这里我们进行一个讲解。 

重载:

        两个函数在同一个作用域中。

        函数名相同,参数不同(类型不同,顺序不同,个数不同)。

重写(覆盖):

        两个函数分别在基类和派生类中。 

        两个函数都是虚函数。

        函数名、返回类型、参数列表相同(两个例外除外)。

重定义(隐藏):

        两个函数分别在基类和派生类中。

        函数名相同。

        不符合重写就是重定义。

三.抽象类 

在虚函数后面加上=0虚函数称为纯虚函数,包含纯虚函数的类又被称为抽象类(也叫接口类),抽象类是不能实例化出对象的,派生类继承后也不能实例化对象,只有重写基类中的纯虚函数才能实例化出对象。纯虚函数规定了派生类必须重写

#include<iostream>
using namespace std;//抽象类
class Person//不能实例化出对象
{
public://纯虚函数virtual void BuyTicket() = 0;
};class Student :public Person
{
public:virtual void BuyTicket()//对基类的纯虚函数进行重写后,派生类才能实例化出对象{cout << "学  生:买票-半价" << endl;}
};void func(Person& tmp)
{tmp.BuyTicket();
}int main()
{Student s;func(s);return 0;
}

1.接口继承与实现继承 

在继承体系中,对于普通函数来说,派生类继承的是函数的实现,而对于虚函数来说,派生类继承的是函数的接口,这句话如何理解呢,我们来看代码来理解。 

在上面的代码中,派生类B中,重写了A的函数,但是这种重写指定是重写了函数的实现,而函数的结果依然是基类的函数接口,所以说重写是一种接口继承。 

四.多态原理 

学会了多态的使用,接下来再来学一下多态的原理。注意!!!本代码都是在32位机器下进行解释的,64的机器会略有不同,但都大同小异。

1.虚函数表 

在解释多态原理前,我们先来看两段代码。 

 在上面的两段代码中,我们分别定义了两个类,一个类中有一个普通的成员函数,而另一个类中有一个虚函数,再通过求它们的大小。

可以看到有虚函数和没有虚函数的类大小是不一样的,为什么呢?

因为在有虚函数的类中,会多了一个虚函数表指针,这个虚函数表指针指向一个虚函数表,虚函数表中存储着类中所有的虚函数的地址

现在知道了,如果类中有虚函数,那么类对象中就会存一个虚函数表指针,接下来再看看继承关系下又是怎样的。

通过上面的图,我们可以看到,在A对象的虚函数表中,只会存储虚函数的地址,而普通函数的地址不会存储到虚函数表中,在看看B对象,在B对象的虚函数表中,会存储继承A下来的虚函数,但是不同的是,在B对象中,重写了A中的func1,所以B对象虚函数表中,func1是B重写下来的func1,即图中红色圈圈的位置(可能有点小,可以放大来看)。 

总结 

在有虚函数的类对象中,对象里面会存一个虚函数表指针,虚函数表指针又指向一个虚函数表,其实这个虚函数表本质上是一个函数指针数组,这个函数指针数组是以nullptr来结尾的,同时在派生类的虚函数表中,会继承下基类的虚函数,如果在派生类中重写了某个虚函数,则重写后的虚函数会覆盖到派生类的虚函数表中。

同时,虚函数表是存在代码段中的,虚函数也是存在代码段中的。

2.多态的原理

看完上面的代码及分析,那么多态的原理到底是什么呢? 我们来结合下面的代码来看一下。

#include<iostream>
using namespace std;
class Person
{
public:virtual void BuyTicket(){cout << "普通人:买票-全价" << endl;}
};
class Student :public Person
{
public:virtual void BuyTicket(){cout << "学  生:买票-半价" << endl;}
};
void func(Person& tmp)
{tmp.BuyTicket();
}
int main()
{Person p;Student t;func(p);func(t);return 0;
}

经过上面的分析,我们可以知道p和t的虚函数表是下面这样的。

p对象的虚函数表中,存的是Person::BuyTicket,而t对象的虚函数表中存的是Student::BuyTicket,因为在Student类中,重写了BuyTicket函数,当我们调用func函数的时候,因为传的是指针或者引用,所以实际上Person& tmp是传谁,tmp就指向谁,这样就能达到传谁调用谁的虚函数,因为tmp会到对应的对象的虚函数表中去找到对应的虚函数

3.静态绑定与动态绑定 

有的同学可能会知道,多态有编译时的多态运行时的多态,也可以说叫静态绑定动态绑定,那么这两个又有什么区别呢?

静态绑定:也称编译时的多态,即代码在编译后就已经确定的,例如函数重载,在程序中,当你调用一个重载的函数的时候,编译时就已经可以确定调用那个函数了。

动态绑定:也称运行时的多态,即要在代码运行后才能确定的,例如上面讲到的虚函数,当我们调用func函数时,func里面又通过tmp去调用另一个函数,但是调用的这个函数是不确定的 有可能是Person的也有可能是Student的传谁就调用谁由于不确定,所以要在运行时,到对应的虚函数表中去找,即运行时的多态


我们也可以通过看汇编代码来学习。当调用Print函数时,因为Print是普通函数,所以在编译时就确定了,直接调用就行,而形成多态的虚函数,要通过一系列的操作到对应的虚函数表中去找到对应的虚函数

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

在上面的讲解中,我们看到的都是派生类中只有基类的虚函数重写,而派生类中没有不是继承基类的虚函数,所以接下来我们来看一下,在单继承和多继承中的虚函数表表。

1.单继承中的虚函数表 

我们先来看一下代码。

 

在上面的代码中,基类A只有两个虚函数func1、func2,而在派生类中,重写了虚函数func1,同时又多了两个虚函数func3、func4,但是我们从vs的监视窗口中,并没有看到func3和func4,那么是不是代表这两个虚函数不存在呢,其实不是的,只不过是在vs的监视窗口中没有显示出来罢了。我们也可以通过写一个代码来证明。 


通过学习了上面的知识,我们知道了在一个类对象中,虚函数表指针是存储对象的第一个位置的,所以我们通过获取第一个位置的数据,即虚函数表指针来找到虚函数表,再把虚函数表打印出来。 

#include<iostream>
using namespace std;class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}virtual void func2(){cout << "A::func2()" << endl;}
};
class B :public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}virtual void func3(){cout << "B::func3()" << endl;}virtual void func4(){cout << "B::func4()" << endl;}
};typedef void(*VFTable) ();//函数指针void Print(VFTable* table)
{for (int i = 0; *(table+i) != 0; i++){cout << *(table+i) << "->";VFTable f = table[i];//这段代码表示,一个f的函数指针指向一个虚函数f();//使用函数指针调用指向的函数}
}int main()
{A a;B b;VFTable* p = (VFTable*)(*((int*)(&b)));//将虚函数表指针指向的第一个虚函数取出来Print(p);return 0;
}

结果如下: 

可以看到其实在B对象中,是由func3和func4两个虚函数的,只不过vs的监视窗口没有显示出来而已。

2.多继承中的虚函数表 

 看完了单继承的情况,我们再来看看多继承的情况,如下:

 

 

在多继承的情况中,一个派生类会有两个虚函数指针,其虚函数指针指向的虚函数表的内容如图中所示,但是我们没有看到C类对象的func3函数,那是因为情况与单继承的一样,只是vs的监视窗口没有显示出来而已,我们同样使用单继承的方式,可以将两个虚函数表打印出来。

代码如下:

#include<iostream>
using namespace std;class A
{
public:virtual void func1() {cout << "A::func1()" << endl;}virtual void func2() {cout << "A::func2()" << endl;}
};class B
{
public:virtual void func1() {cout << "B::func1()" << endl;}virtual void func2() {cout << "B::func2()" << endl;}
};class C :public A, public B
{
public:virtual void func1() {cout << "C::func1()" << endl;}virtual void func3() {cout << "C::func3()" << endl;}
};typedef void(*VFTable)();void Print(VFTable* tmp)
{for (int i = 0; *(tmp + i) != nullptr; i++){cout << *(tmp + i) << "->";VFTable f = *(tmp + i);f();}cout << endl;
}int main()
{C c;VFTable* p1 = (VFTable*)(*((int*)(&c)));//第一张虚函数表的第一个虚函数的地址VFTable* p2 = (VFTable*)(*(int*)((char*)(&c) + sizeof(A)));//第二张虚函数表的第一个虚函数的地址Print(p1);Print(p2);return 0;
}

运行效果如下:

 

通过运行结果,我们看到在C类对象的func3虚函数是存在第一张虚函数表中。


当然对于多继承的情况还有菱形继承菱形虚拟继承,这两种情况对于上面来说过于复杂,同时,一般来说,菱形继承很少用,也不好用,这里就不做解释了,如果以后博主有空,可能会补齐一下菱形继承和菱形虚拟继承的情况。看到这里,多态就已经解释完了。 

相关文章:

  • es数据备份和迁移Elasticsearch
  • 邮件系统中的CC和BCC含义
  • 第十章 DashBoard
  • 从零开始傅里叶变换
  • Design-expert中的ANOVA分析
  • 搜维尔科技:第九届元宇宙数字人设计大赛入围作品名单
  • 如何设计与构建企业IT数字化运维体系与目标
  • 使用 ASM 修改字段类型,解决闪退问题
  • 如何*永久*禁用edge打开PDF文件?
  • 高中数学:平面向量-数量积(向量与向量的乘积)与投影
  • Spring Cloud Alibaba-08-SMS短信服务
  • 逐元素比较
  • CSS动画技术(css-animation)
  • 于ThinkPHP开发的赛事报名小程序
  • C语言游戏实战(12):植物大战僵尸(坤版)
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • CAP理论的例子讲解
  • co模块的前端实现
  • Debian下无root权限使用Python访问Oracle
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • MYSQL如何对数据进行自动化升级--以如果某数据表存在并且某字段不存在时则执行更新操作为例...
  • ng6--错误信息小结(持续更新)
  • 等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规
  • 记录:CentOS7.2配置LNMP环境记录
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 前端设计模式
  • 驱动程序原理
  • 数组大概知多少
  • 3月27日云栖精选夜读 | 从 “城市大脑”实践,瞭望未来城市源起 ...
  • ​LeetCode解法汇总518. 零钱兑换 II
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • #Linux(Source Insight安装及工程建立)
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • (02)vite环境变量配置
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (创新)基于VMD-CNN-BiLSTM的电力负荷预测—代码+数据
  • (二)延时任务篇——通过redis的key监听,实现延迟任务实战
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (六)软件测试分工
  • (欧拉)openEuler系统添加网卡文件配置流程、(欧拉)openEuler系统手动配置ipv6地址流程、(欧拉)openEuler系统网络管理说明
  • (详细文档!)javaswing图书管理系统+mysql数据库
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET Framework与.NET Framework SDK有什么不同?
  • .net反混淆脱壳工具de4dot的使用
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
  • .net下的富文本编辑器FCKeditor的配置方法
  • .sdf和.msp文件读取
  • @FeignClient注解,fallback和fallbackFactory
  • @SpringBootApplication 注解
  • [ 2222 ]http://e.eqxiu.com/s/wJMf15Ku
  • [100天算法】-不同路径 III(day 73)
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [2008][note]腔内级联拉曼发射的,二极管泵浦多频调Q laser——
  • [20171101]rman to destination.txt