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

news2025/5/25 10:52:32

在本篇博客中,作者将会带领你深入理解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.coloradmin.cn/o/1682977.html

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

相关文章

patroni 部分源码阅读

问题1 /usr/local/lib/python3.9/site-packages/patroni/postgresql/init.py 964 contextmanager 965 def get_replication_connection_cursor(self, hostNone, port5432, **kwargs): 966 conn_kwargs self.config.replication.copy() 967 conn_kwar…

【Kubernetes】kubectl详解

陈述式资源管理方法&#xff1a; 1.kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口 2.kubectl 是官方的CLI命令行工具&#xff0c;用于与 apiserver 进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为 apiserver 能识别的…

JavaWeb基础(HTML,CSS,JS)

这些知识用了三四天左右学完&#xff0c;因为是JavaWeb&#xff0c;并不是前端&#xff0c;所以只是够用&#xff0c;不是深入&#xff0c;但是这确实是学校一个学期交的东西&#xff08;JavaWeb课程&#xff09;。 总结一下网页分为三部分&#xff1a;HTML(内容结构),CSS&…

HTML | 在IDEA中配置Tomcat时遇到的一些问题的解决办法

目录 IDEA中没有web文件夹目录 Tomcat在哪里配置服务器 IDEA中没有web文件夹目录 首先说在IDEA中没有web这个文件夹的解决办法 在菜单栏中帮助中点击查找操作搜索添加框架支持&#xff08;因为我的IDEA会出现无法点击这个操作&#xff0c;所以我对该操作添加了快捷键&#xf…

linux系统内存持续飙高的排查方法

目录 前言&#xff1a; 1、查看系统内存的占用情况 2、找出占用内存高的进程 3、解决方法 4、补充&#xff1a;如果物理内存使用完了&#xff0c;会发生的情况 前言&#xff1a; 如果一台服务器内存使用率持续处于一个高峰值&#xff0c;服务器可能会出现响应慢问题。例如s…

03:PostgreSQL逻辑结构(表空间、数据库、模式、表、索引)

环境规划&#xff1a; 操作系统&#xff1a;CentOS 7.9 64bitPostgreSQL 版本&#xff1a;16.x 或 15.x安装用户&#xff1a;postgres软件安装目标路径&#xff1a;/usr/pgsql-<version>数据库数据目录&#xff1a;/pgdata 目录 表空间Tablespace 默认表空间 手动创建…

【Vue】性能优化

使用 key 对于通过循环生成的列表&#xff0c;应给每个列表项一个稳定且唯一的 key&#xff0c;这有利于在列表变动时&#xff0c;尽量少的删除和新增元素。 使用冻结的对象 冻结的对象&#xff08;Object.freeze(obj)&#xff09;不会被响应化&#xff0c;不可变。 使用函…

【贪心算法题目】

1. 柠檬水找零 这一个题目是一个比较简单的模拟算法&#xff0c;只需要根据手里的钱进行找零即可&#xff0c;对于贪心的这一点&#xff0c;主要是在20元钱找零的情况下&#xff0c;此时会出现两种情况&#xff1a;10 5 的组合 和 5 5 5 的组合&#xff0c;根据找零的特点&a…

通过管理系统完成商品属性维护

文章目录 1.数据库表设计1.商品属性表 2.renren-generator生成CRUD1.基本配置检查1.generator.properties2.application.yml 2.启动RenrenGeneratorApplication.java生成CRUD1.启动后访问localhost:812.生成商品属性表的crud 3.将crud代码集成到项目中1.解压&#xff0c;找到ma…

Gittee

前言&#xff1a; 海鸥禁止 git简述 分布式版本控制系统 版本管理 集中式只有一个档案馆 分布式可以每人有一个档案馆&#xff0c;版本合并 协同工作 github&#xff0c;gitlab&#xff0c;gitee是git的托管平台 安装git 略 添加&#xff0c;提交文件 推到远程仓库 常…

vscode安装多版本esp-idf

安装 离线安装 vscode设置 建立一个新的配置文件, 这里面的插件是全新的 安装esp-idf 官网下载espidf 安装这一个 选项默认即可 记住各一个路径, 之后要用到 vscode安装插件 安装以后会进入这一个界面, 也可以CtrlShiftP输入ESP-IDFextension进入 使用espressif 问题 这一个…

微信小程序---小程序文档配置(2)

一、小程序文档配置 1、小程序的目录结构 1.1、目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page 一个小程序主体部分由三个文件组成&#xff0c;必须放在项目的根目录 比如当前我们的《第一个小程序》项目根目录下就存在这三个文件&#xff1a; 1…

【论文速读】|探索ChatGPT在软件安全应用中的局限性

本次分享论文&#xff1a;Exploring the Limits of ChatGPT in Software Security Applications 基本信息 原文作者&#xff1a;Fangzhou Wu, Qingzhao Zhang, Ati Priya Bajaj, Tiffany Bao, Ning Zhang, Ruoyu "Fish" Wang, Chaowei Xiao 作者单位&#xff1a;威…

day08-Java常用API

day08——Java常用API 一、今日内容介绍、API概述 各位同学&#xff0c;我们前面已经学习了面向对象编程&#xff0c;使用面向编程这个套路&#xff0c;我们需要自己写类&#xff0c;然后创建对象来解决问题。但是在以后的实际开发中&#xff0c;更多的时候&#xff0c;我们是…

Linux软硬链接及动静态库

软硬链接与动静态库 软连接 创建链接的方法&#xff1a; ln -s test1.txt test2.txt 其中ln 是link(链接)&#xff0c;-s 是soft(软)&#xff0c;后者链接前者。 此时打开test2.txt&#xff0c;发现其中内容与test.txt一致。那么软连接到底建立了什么联系&#xff1f;…

Python函数进阶:四大高阶函数、匿名函数、枚举、拉链与递归详解

系列文章目录 Python数据类型&#xff1a;编程新手的必修课深入探索Python字符串&#xff1a;技巧、方法与实战Python 函数基础详解Python正则表达式详解&#xff1a;掌握文本匹配的魔法Python文件操作宝典&#xff1a;一步步教你玩转文件读写Python面向对象基础与魔法方法详解…

添加webpack.config.js配置

webpack 命令默认会去根目录查找webpack.config.js配置文件&#xff0c;如果没有&#xff0c;则会使用webpack默认的零配置打包规则进行打包&#xff0c;默认的零配置打包规则主要包括下面这几点&#xff1a; 1. 默认入口文件&#xff1a;Webpack 默认会将 ./src/index.js 作为…

“壕无人性”的沙特也要买量子计算机!巨头沙特阿美的合作方竟是它?

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨浪味仙 排版丨沛贤 深度好文&#xff1a;1200字丨5分钟阅读 摘要&#xff1a;石油巨头沙特阿美与 Pasqal 开启合作&#xff0c;计划于 2025 年部署一台 200 量子比特的量子计算机&#xff…

开源大模型与闭源大模型:技术哲学的较量

目录 前言一、 开源大模型的优势1. 社区支持与合作1.1 全球协作网络1.2 快速迭代与创新1.3 共享最佳实践 2. 透明性与可信赖性2.1 审计与验证2.2 减少偏见与错误2.3 安全性提升 3. 低成本与易访问性3.1 降低研发成本3.2 易于定制化3.3 教育资源丰富 4. 促进标准化5. 推动技术进…

【qt】QDockWidget 浮动窗口

QDockWidget 浮动窗口 一.QDockWidget 的用法 前言&#xff1a;很简单&#xff0c;放心食用 一.QDockWidget 的用法 太简单了&#xff0c;直接来吧&#xff01; 直接做个小项目来了解QDockWidget 的用法 目标效果图&#xff1a; 开始拖放&#xff1a; 开始布局&#xff1a; …