C++特性之多态

news2025/2/23 17:09:08

C++作为面向对象的语言,三大特性之一多态在平时的编程中使用频率特别高。

本篇文章就来详细讲解一下多态。

什么是多态

不同的对象相同的一件事会出现不同的状态,这就是多态

举个列子:比如普通人买车票要全价购买,而军人只用半价,这就是多态的一种体现。

多态的定义及实现

多态的构成条件

想要实现多态,需要以下条件

  • 通过基类的指针或者引用来调用子类对象的虚函数
  • 被调用的函数必须是虚函数

虚函数:类成员函数前用 virtual 关键字修饰的函数

 我们可以实际看看如何实现多态。

#include<iostream>

using namespace std;

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

int main()
{
	Person* p1;
	Person p2;
	Student s1;
	p1 = &p2;
	p1->Ticket();
	p1 = &s1;
	p1->Ticket();
}

我们发现,通过基类的指针调用虚函数能够实现多态。

我们还能通过引用实现多态。

#include<iostream>

using namespace std;

class Person {
public:
	virtual void Ticket()
	{
		cout << "全价" << endl;
	}
};
class Student : public Person {
public:
	virtual void Ticket()
	{
		cout << "半价" << endl;
	}
};
void Func(Person& p)
{
	p.Ticket();
}

int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

多态的调用更关注的是指针指向的类型,和普通调用不同,普通调用更关注的是调用的类型

虚函数的重写

在继承中,若是派生类继承了基类,那么基类的同名成员会和派生类的同名成员构成隐藏关系

而在多态中, 这种虚函数之间的同名成员则称作重写(覆盖)。

比如上面的例子中, Student 类就重写了Person类的 Ticket 函数。

一般虚函数构成重写需要三个条件:函数名相同,返回值相同,参数相同。

只要基类和派生类有同名函数,且基类函数前加了virtual,那么派生类的函数也会成虚函数。

不过虚函数的重写也有例外。

协变

重写的虚函数的返回值不同,即基类的虚函数返回基类对象的指针派生类返回派生类对象的指针时,即构成协变。

#include<iostream>

using namespace std;

class A{};
class B : public A {

};

class Person {
public:
	virtual A* Ticket()
	{
		cout << "全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual B* Ticket()
	{
		cout << "半价" << endl;
		return nullptr;
	}
};

int main()
{
	Person* p1;
	Person p2;
	Student s1;
	p1 = &p2;
	p1->Ticket();
	p1 = &s1;
	p1->Ticket();
	return 0;
}

 比如这里,Ticket 虚函数的重写就构成了协变。

Person 类的 Ticket 函数返回 A 类的指针,Student 类的 Tciket 函数返回 B类的指针。

而如果不是基类返回基类的指针,派生类返回派生类的指针,则不会构成协变,还会报错。

 析构函数的重写

若是基类的析构函数是虚函数,那么派生类的析构函数无论加不加virtual 关键字修饰都会是虚函数。

虽然两个析构函数的名称不同,但是二者之间依旧构成重写。

析构函数构成重写的原因

编译器对析构函数做了特殊处理,所有析构函数都会在编译后转换为 destructor;

 override 和 final

由于多态会导致函数重写,有时又会因为程序员的原因导致函数字母出错,从而无法重写函数,这类错误只能在运行的时候发现,因此C++提供了两个关键字用来检测函数是否重写。

  • override : 检查派生类函数是否重写,没重写则报错。
  • final : 修饰虚函数,表示该虚函数不能被重写,修饰普通函数,表示该函数不能被继承

override 示例 

 final 示例

抽象类 

由于多态特性的存在,C++还有一个抽象类的概念。

概念:类中有一个函数是纯虚函数。包含纯虚函数的类就是抽象类,这种类无法实例化对象

纯虚函数:虚函数后面加上 =0 ,就是纯虚函数。

 派生类继承了抽象类后也无法实例化对象,必须重写纯虚函数才能实例化对象。

像纯虚函数这样的函数体现了接口继承。

像图中的 Animal 类,Animal 肯定不能作为一个对象存在在自然界中,而 Cat 当然存在于自然中,C++的抽象类也是类似,无法实例化对象。

接口继承和实现继承

普通类的继承都是实现继承,派生类继承了基类的函数,能够使用基类的函数,而虚函数的继承则体现了接口继承,虚函数的存在是为了让派生类重写,达成多态。因此除非是为了实现多态,一般是不用虚函数的。

多态原理

一般编译器对于只有函数的空类只会给它一个字节的标记位,但是这里 A 类有一个虚函数,它的却有8字节的大小。 

这是因为当一个类含有虚函数时,编译器就会给该类提供一个虚函数表该类实例化的对象中都会有一个指针用来指向该虚函数表

这个指针一般就在对象的头4个或8个字节中;

根据编译器的环境,这个指针大小可能为4或8字节大小。

多态的原理就是建立在虚函数表之上的。

class A
{
public:
	virtual void Test1()
	{

	}
	virtual void Test2()
	{
	}
	void Test3()
	{

	}
};

class B : public A
{
public:
	virtual void Test1()
	{

	}
};
int main()
{
	B b;
	A a;
	return 0;
}

我们写下以上的代码,然后通过监视窗口,可以看到,b对象和a对象都分别有一个虚函数表(_vfptr),其中 B 类 重写了 Test1 函数,因此它的虚表中 Test1 函数就是 B类中的Test1函数,而Test2 没有重写,因此就是 A 类中的 Test2 函数。 

观察总结:

  • 派生类生成虚表会先将基类虚表内容保存到派生类的虚表内容中
  • 若是派生类已经重写了基类的某个虚函数,则会将重写的虚函数覆盖到虚表中基类的虚函数。
  • 而派生类自己新增的虚函数则按派生类的声明顺序新增到派生类虚表的最后。

从观察总结中延伸,我们就明白了多态的原理。

  • 基类的指针或引用的是派生类时它的虚表指针指向的就是派生类的虚表,其中保存的虚函数已经覆盖或者重写完毕了。
  • 当基类指针或引用的是基类时,它的虚表指针指向的就是基类的虚表,其中保存的虚函数也是基类的虚函数。
  • 这样就实现了一个对象去完成同一行为时展现的不同形态。

 动态绑定和静态绑定

动态绑定:程序运行期间根据具体的类型确定行为,调用具体的函数。

静态绑定:编译期间,就已经确定了程序的行为。

而多态调用就是典型的动态绑定,普通调用就是静态绑定。

通过查看多态调用和普通调用的汇编代码就能发现,多态调用只有在运行的时候才会确定调用的是哪个函数。

通过反汇编也能够看到多态调用和普通调用不同。

普通调用直接就找到了函数位置,而多态调用在运行的时候才会去连接派生类的虚函数表,再通过虚函数表找到对应的函数位置。

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

单继承的虚函数表

class A
{
public:
	virtual void Test1()
	{

	}
	virtual void Test2()
	{
	}
	void Test3()
	{

	}
};

class B : public A
{
public:
	virtual void Test1()
	{

	}
	virtual void Test3()
	{

	}
	virtual void Test4()
	{

	}
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	B b;
	A a;

	PrintVTable((VFPTR*)(*(int*)&b));
	PrintVTable((VFPTR*)(*(int*)&a));

	return 0;
}

在 x86 环境下查看虚函数表的内容。

其中 B 类重写了 A 类的 Test1 函数,Test2 函数未重写。

来看看 a 和 b 的虚函数有什么不同。

我们发现, B 类的 Test1 函数是重写后的地址, 而 Test2 函数的地址和 A 类的地址相同。

而 B 类后续的虚函数则添加到 B 类虚表的后面。 

多继承的虚函数表


class A
{
public:
	virtual void Test1()
	{
		cout << "A::Test1()" << endl;
	}
	virtual void Test2()
	{
		cout << "A::Test2()" << endl;
	}
	void Test3()
	{
		cout << "A::Test3()" << endl;

	}
};

class B
{
public:
	virtual void Test1()
	{
		cout << "B::Test1()" << endl;

	}
	virtual void Test2()
	{
		cout << "B::Test2()" << endl;
	}
	virtual void Test4()
	{
		cout << "B::Test4()" << endl;
	}
};

class C : public A, public B
{
public:
	virtual void Test1()
	{
		cout << "C::Test1()" << endl;

	}
	virtual void Test3()
	{
		cout << "C::Test3()" << endl;
	}
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	C c;

	PrintVTable((VFPTR*)(*(int*)&c));
	PrintVTable((VFPTR*)*(int*)((char*)&c + sizeof(A)));
	return 0;
}

我们发现,如果一个类继承的两个函数都有虚函数,那么派生类就会创立两个虚表,分别保存基类的虚函数并且进行了重写操作。

比如这里 C类的 Test1 函数就分别重写在 A类的虚表上和 B 类的虚表上。

而没有重写的Test2函数就被保存了下来。

总结

当一个类有虚函数时,编译器就会为这个类创建一个虚表该类实例化的对象都共享这个虚表,每个对象的头4个(或8个)字节就保存了这个虚表的地址。

虚函数表存在只读代码段中,虚函数也是,而虚函数表指针是在类的构造函数中赋值的,因此类的构造函数不能为虚函数。

一个类继承了一个有虚表的类通过基类的指针引用来调用构成重写的虚函数时,就能实现多态。

一个类继承多个有虚表的类,该类就有多个虚表有重写的虚函数就会分别添加到对应的虚函数,比如 A 类的虚表中有函数被派生类重写了,那就添加到 A 类的虚表中。而没有重写的虚函数添加到继承顺序中第一个有虚表的基类的虚表中。

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

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

相关文章

POJ 3735 Training little cats 动态规划(矩阵的幂)

一、题目大意 我们有N只猫&#xff0c;每次循环进行K次操作&#xff08;N<100&#xff0c;K<100&#xff09;&#xff0c;每次操作可有以下三种选择&#xff1a; 1、g i 给第i只猫1个食物 2、e i 让第i只猫吃完它所有的食物 3、s i j 交换第i和j只猫的食物。 求出M次…

【池式组件】线程池的原理与实现

线程池的原理与实现 线程池简介1.线程池1.线程池2.数量固定的原因3.线程数量如何确定4.为什么需要线程池5.线程池结构 线程池的实现数据结构设计1.任务结构2.任务队列结构3.线程池结构 接口设计 线程池的应用reactorredis 中线程池skynet 中线程池 线程池简介 1.线程池 1.线程…

ABP vNext 扩展 CurrentUser

ABP内置Users表&#xff0c;我们可以对其字段进行扩展&#xff0c;辅助进行更详细的数据记录 ICurrentUser 是主要的服务,用于获取有关当前活动的用户信息. 以下是 ICurrentUser 接口的基本属性:1. IsAuthenticated 如果当前用户已登录(已认证),则返回 true. 如果用户尚未登录…

软件设计师——面向对象技术(一)

&#x1f4d1;前言 本文主要是【面向对象技术】——软件设计师—面向对象技术的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是听风与他&#x1f947; ☁️博客首页&#xff1a;CSDN主页听风与他 &#…

每日一练2023.12.8—— 稳赢【PTA】

题目链接&#xff1a; L1-044 稳赢 题目要求&#xff1a; 大家应该都会玩“锤子剪刀布”的游戏&#xff1a;两人同时给出手势&#xff0c;胜负规则如图所示&#xff1a; 现要求你编写一个稳赢不输的程序&#xff0c;根据对方的出招&#xff0c;给出对应的赢招。但是&#xff…

正则表达式(7):转义符

正则表达式&#xff08;7&#xff09;&#xff1a;正则表达式&#xff08;5&#xff09;&#xff1a;转义符 本博文转载自 此处&#xff0c;我们来认识一个常用符号&#xff0c;它就是反斜杠 “\” 反斜杠有什么作用呢&#xff1f;先不着急解释&#xff0c;先来看个小例子。 …

Python random模块及用法

random 模块主要包含生成伪随机数的各种功能变量和函数。 在 Python 的交互式解释器中先导入 random 模块&#xff0c;然后输入 random.__all__ 命令&#xff08;__all__ 变量代表了该模块开放的公开接口&#xff09;&#xff0c;即可看到该模块所包含的全部属性和函数&#x…

【分享】我想上手机器学习

目录 前言 一、理解机器学习 1.1 机器学习的目的 1.2 机器学习的模型 1.3 机器学习的数据 二、学习机器学习要学什么 2.1 学习机器学习的核心内容 2.2 怎么选择模型 2.3 怎么获取训练数据 2.4 怎么训练模型 三、机器学习的门槛 3.1 机器学习的第一道门槛 3.2 机器…

ES-环境安装(elasticsearch:7.17.9,kibana,elasticsearch-head)

ES 环境搭建 1 拉取镜像 常用三件套 docker pull kibana:7.17.9 docker pull elasticsearch:7.17.9 docker pull mobz/elasticsearch-head:52 启动镜像 elasticsearch 安装 这里可以先不挂载文件启动一波&#xff0c;然后把容器里的文件拷贝出来 docker run -p 19200:9200 …

[oeasy]python0002_终端_CLI_GUI_编程环境_游戏_真实_元宇宙

回忆 上次 了解了 python 语言的特点 历史悠久功能强大深受好评已成趋势 3大主流操作系统 macwindowslinux 我们 选择 linux 作为基础系统 为什么选择 黑乎乎的命令行界面呢&#xff1f;&#x1f914; GUI vs CLI 个人电脑 用图标和菜单组成 图形界面(GUI) Graphic User I…

Numpy数组的重塑,转置与切片 (第6讲)

Numpy数组的重塑,转置与切片 (第6讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ�…

朴素贝叶斯 朴素贝叶斯原理

朴素贝叶斯 朴素贝叶斯原理 判别模型和生成模型 监督学习方法又分生成方法 (Generative approach) 和判别方法 (Discriminative approach)所学到的模型分别称为生成模型 (Generative Model) 和判别模型 (Discriminative Model)。 朴素贝叶斯原理 朴素贝叶斯法是典型的生成学习…

鸿蒙OS应用开发之最简单的程序

鸿蒙OS应用开发之最简单的程序 前面介绍怎么样安装鸿蒙应用开发的环境&#xff0c;然后试着运行起来&#xff0c;并安装运行的虚拟机&#xff0c;以及对应9.0版本的API和SDK等软件。这样就具备了基本的开发基础&#xff0c;就可以进入创建应用程序开发了。 在我们起飞之前&…

【Java基础系列】Cron表达式入门

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

通过kubeadm方式安装k8s

虚拟机最少是 2 core&#xff0c;master内存最小3G&#xff0c;node内存最小2G. 要求的Docker版本是18.03&#xff0c;如果不是安装的docker ce&#xff0c;版本是过旧的&#xff0c;可以选择删除后重新安装&#xff1b; 也可以重新创建一个虚拟机执行以下命令。 简单方法&am…

解决“使用command+shift+a 总是弹出默认终端”

冲突出现的终端如下 问题原因 MacOS下使用IntelliJ 系列的IDE就是经常遇到这个问题&#xff0c;原因该快捷键与系统的 《在“终端”中搜索man页面索引 》功能的快捷键冲突了&#xff0c;Find Action是一个很高频使用的&#xff01; 解决方案 把系统《在“终端”中搜索man…

5组10个共50个音频可视化效果PR音乐视频制作模板

我们常常看到的图形跟着音乐跳动&#xff0c;非常有节奏感&#xff0c;那这个是怎么做到的呢&#xff1f;5组10个共50个音频可视化效果PR音乐视频制作模板满足你的制作需求。 PR音乐模板|10个音频可视化视频制作模板05 https://prmuban.com/36704.html 10个音频可视化视频制作…

Python语言求解嵌套列表中的最大元素和

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在处理嵌套列表时&#xff0c;有时我们需要找到列表中的最大元素以及对应的位置。本文将深入讨论如何使用Python有效地解决这个问题。我们将使用不同的方法&#xff0c;包括递归、列表推导和NumPy库&#xff0c;…

我有才打造私域流量的知识付费小程序平台

在当今数字化时代&#xff0c;知识付费市场正在迅速崛起&#xff0c;而私域流量的概念也日益受到重视。私域流量指的是企业通过自有渠道获取的、能够自由支配的流量&#xff0c;这种流量具有更高的用户粘性和转化率。因此&#xff0c;打造一个基于私域流量的知识付费小程序平台…

VBA_MF系列技术资料1-237

MF系列VBA技术资料 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04属于定…