【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虚函数是存在第一张虚函数表中。


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

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

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

相关文章

简易CAD程序:Qt多文档程序的一种实现

注&#xff1a;文中所列代码质量不高&#xff0c;但不影响演示我的思路 实现思路说明 实现DemoApplication 相当于MFC中CWinAppEx的派生类&#xff0c;暂时没加什么功能。 DemoApplication.h #pragma once#include <QtWidgets/QApplication>//相当于MFC中CWinAppEx的派生…

以太坊(3)——智能合约

智能合约 首先明确一下几个说法&#xff08;说法不严谨&#xff0c;为了介绍清晰才说的&#xff09;&#xff1a; 全节点矿工 节点账户 智能合约是基于Solidity语言编写的 学习Solidity语言可以到WFT学院官网&#xff08;Hello from WTF Academy | WTF Academy&#xff09;…

以x为界,分隔链表为两个分区,各分区内元素相对位置不变

题目描述&#xff1a; 题目思路&#xff1a; 1.设置两个头指针表示两个分区&#xff0c;并对每个分区设置相对于的遍历指针&#xff0c;指向分区链表链尾 2.设置原链表的遍历指针&#xff0c;判断指针所指元素是否小于目标值x&#xff0c;小于的话将结点添加到第一个分区末尾…

搭建电商电子商务平台有哪些好用的电商API数据采集接口?

电商API接口主要用于帮助开发者将电商功能集成到自己的应用程序中&#xff0c;实现诸如商品检索、商品价格数据获取、订单处理、支付、物流跟踪等功能。以下是一些常用的电商API接口提供商&#xff1a; 主流电商平台API&#xff1a; 淘宝开放平台&#xff1a;提供淘宝、天猫、…

ngnix 入门 二,docker启动nginx, 安装ssl 证书,使用配置文件,映射后端服务 ,提供给前端项目访问

搭建生产环境真不是人做的事&#xff0c;特别是对于一知半解的人。仅以此文献给各位技术人 说一下背景&#xff1a;项目前后端分离&#xff0c;前端 vue3 、小程序端 &#xff0c;后端 go 提供服务。 微信小程序需要使用 https 请求。 这就必须让我们想到nginx 了 想要达到的…

MySQL学习之DQL语句(数据查询语言)

准备SQL CREATE TABLE student ( id int, -- 编号 name varchar(20), -- 姓名 age int, -- 年龄 sex varchar(5), -- 性别 address varchar(100), -- 地址 math int, -- 数学 english int -- 英语 );INSERT INTO student(id,NAME,age,sex,address,math,english) VALUES (1,…

【UE5.1 角色练习】02-添加慢走、快速跑、蹲伏功能

目录 前言 步骤 一、慢走 二、快速跑 三、蹲伏 前言 在上一篇文章基础上&#xff08;【UE5.1 角色练习】01-使用小白人蓝图控制商城角色移动&#xff09;继续实现角色的慢走、快速跑以及蹲伏功能 步骤 一、慢走 1. 打开项目设置&#xff0c;添加一个操作映射&#x…

思维导图-VPN

浏览器集成了受信任的机构的证书

linux---信号的捕捉和处理

提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、信号 可以简单理解为信号是一个进程给另一个信号发消息&#xff0c;进程收到对应的信号就执行对应的方法&#xff0c;linux信号可以分为实时信号和非实时信号 1-31为非实时信号&#xff0c;34-64为…

计算机精选期刊特辑

文章目录 一、征稿简介二、合作期刊三、投稿咨询四、咨询 一、征稿简介 艾思科蓝依托互联网信息与数据库技术、整合渠道与合作资源&#xff0c;提供EI/SCI/SCIE/SSCI期刊论文的内容审查、发表支持等服务。艾思科蓝与多所知名出版社达成战略合作关系&#xff0c;持续开展合作征…

利用神经网络学习语言(一)——自然语言处理的基本要素

相关说明 这篇文章的大部分内容参考自我的新书《解构大语言模型&#xff1a;从线性回归到通用人工智能》&#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下&#xff1a;regression2chatgpt/ch10_rnn/tokenizer.ipynb 本系列文章将深入探讨一种应用广泛的神经…

syncthing文件夹同步与版本管理

1 前言 syncthing可以用来同步文件夹里的所有文件&#xff0c;并且有不错的版本管理&#xff0c;基本每次更改文件&#xff0c;20-40秒就被扫描到了&#xff0c;非常丝滑&#xff1b;这次以此来同步obsidian的插件和文件&#xff0c;达到多端同步&#xff1b; 我家里有一台台…

Android HAL到Framework

一、为什么需要Framwork? Framework实际上是⼀个应⽤程序的框架&#xff0c;提供了很多服务&#xff1a; 1、丰富⽽⼜可扩展的视图&#xff08;Views&#xff09;&#xff0c; 可以⽤来构建应⽤程序&#xff0c;它包括列表&#xff08;lists&#xff09;&#xff0c;⽹格&am…

闲话 .NET(4):为什么要跨平台?

前言 .NET Core 有一个关键词就是跨平台&#xff0c;为什么要跨平台呢&#xff1f;Windows 操作系统不香吗&#xff1f;今天我们来聊聊这个 原因一&#xff1a;安全考虑 Windows OS 是闭源的&#xff0c;而 Linux 是开源的&#xff0c;因此有些公司的技术负责人就认为 Linux…

如何将老板的游戏机接入阿里云自建K8S跑大模型(下)- 安装nvidia/gpu-operator支持GPU在容器中共享

文章目录 安装nvidia/gpu-operator支持GPU在容器中共享 安装nvidia/gpu-operator支持GPU在容器中共享 安装 nvidia/gpu-operator遇到两个问题&#xff1a; 由于我们都懂的某个原因&#xff0c;导致某些镜像一直现在不成功。 解决办法&#xff0c;准备一个&#x1fa9c;&#…

车间人员作业行为智能检测 AI视觉在生产车间制造中的应用

车间人员作业行为智能检测系统基于神经网络人工智能视觉算法&#xff0c;车间人员作业行为智能检测通过对车间监控摄像头获取的视频图像进行分析和识别&#xff0c;实现了对人员操作行为的智能检测。系统对工人的操作环节进行分解&#xff0c;根据时间、动作标准等方面制定了规…

MemoryDB 2024 论文分享

论文地址点这里。 TL;DR MemoryDB 通过底层依赖 AWS 内部系统 Multi-AZ Transaction Log 实现了 11 个 9 的持久性保证。 通过依赖 Transaction Log 的 Condition API 和租约机制来实现了一致性和可用性保证。 通过周期性调度 Off-box 节点来外部 Rewrite binlog 避免了内存…

C语言基础——循环(2)+关机程序

欢迎点赞支持 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言基础&#xff1b; 文章目录 目录 前言 一、for循环的补充 二、循环的嵌套 1、嵌套的介绍 1.1 练习&#xff1a; 题目解析&#xff1a; 优化&#xff1a; 三、goto语句 1、go…

3DEXPERIENCE DELMIA Role: RFP - Fabrication Robot Programmer

Discipline: Robotics Role: RFP - Fabrication Robot Programmer 在虚拟工厂中定义、验证和编程机器人弧焊和密封剂沉积系统 模拟和验证完整的焊接、密封剂沉积和搬运机器人系统&#xff0c;以消除代价高昂的碰撞并优化生产率提供精确的生产就绪型机器人程序&#xff0c;同…

Day 38 防火墙技术IPtables

一&#xff1a;防火墙简介 1.简介 ​ iptables其实并不是真正的防火墙&#xff0c;我们可以把他理解为一个客户端的代理&#xff0c;用户是通过iptables这个代理&#xff0c;将用户的安全设定执行到对应的“安全框架”中&#xff0c;这个“安全框架”才是真正的防火墙。这个框…