C++——多态(下)

目录

引言

多态

4.多态的原理

4.1 虚函数表指针

4.2 多态的原理

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

5.2 多继承中的虚函数表

结束语


引言

接下来我们继续学习多态。

没有阅读多态(上)的可以点击下面的链接哦~

C++——多态(上)

多态

4.多态的原理

4.1 虚函数表指针

我们先来看看这段代码:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

输出结果为:

我们通过监视来观察一下:

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。它指向的对象就是虚函数表,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

对于大多数现代编译器,在32位系统上,虚指针通常占用4个字节,而在64位系统上则占用8个字节。

编译器可能会为对象添加一些填充以确保内存对齐,从而提高访问速度。因此,实际的对象大小可能会比这些值的简单相加要大。

这就解释了为什么上面的代码输出结果为16。

针对上面的代码我们对其进行改造:

class 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;
}

通过监视来观察一下:

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

5. 总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中                                                                    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数      c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

以下是对几个名词的补充:

虚函数: 虚函数是C++类中的成员函数,它们被声明为virtual,用于支持多态性。虚函数本身和普通函数一样,都是存储在程序的代码段中的。当我们调用一个虚函数时,实际上是在调用存储在代码段中的函数体。

虚表(vtable): 虚表是一个全局的或静态的表,它包含了指向类中所有虚函数的指针。这个表并不是存储在对象中的,而是存储在程序的某个全局或静态数据段中。每个包含虚函数的类都有一个与之对应的虚表。

虚表指针(vptr): 虚表指针是存储在对象实例中的一个特殊指针,它指向该对象所属类的虚表。这个指针是对象的一部分,通常位于对象的起始位置(但这也取决于编译器的具体实现和对象的内存布局)。当我们通过对象的指针或引用来调用虚函数时,编译器会使用这个虚表指针来查找并调用正确的虚函数。

4.2 多态的原理

在C++中,多态性允许我们通过基类指针或引用来调用派生类中的重写方法。为了实现这一点,编译器为每个包含虚函数的类生成一个虚函数表(vtable)。这个表包含了指向类中所有虚函数的指针。

每个对象实例中都含有一个指向其所属类的虚函数表的指针(虚表指针,vptr)。当通过基类指针或引用来调用虚函数时,编译器会使用这个虚表指针来查找并调用正确的虚函数。

(1)对于父类对象,其虚表指针指向基类的虚函数表。

(2)对于派生类对象,其虚表指针指向派生类的虚函数表。如果派生类重写了基类的虚函数,那么派生类的虚函数表中相应位置将指向派生类的重写函数;如果派生类没有重写某个虚函数,那么该位置将指向基类中的原始函数。

因此,当通过基类指针或引用来调用虚函数时,实际调用的函数取决于对象的实际类型(即对象的虚表指针指向的虚函数表)。这种机制允许C++在运行时根据对象的实际类型来确定应该调用哪个虚函数,从而实现多态性。

简单来说就是:

C++通过为每个包含虚函数的类生成一个虚函数表,该表存储了指向类中所有虚函数的指针。每个对象实例内部都含有一个指向其所属类虚函数表的指针。当使用基类指针或引用来调用虚函数时,编译器会利用这个虚表指针,在运行时查找并调用与对象实际类型相匹配的虚函数。

多态实现的两个条件:

(1) 继承(或基类与派生类的关系):继承(或基类与派生类的关系)是多态的基础。它允许一个类(派生类)继承另一个类(基类)的属性和方法。通过继承,派生类可以获取基类的所有公有和保护成员(注意,私有成员对派生类是不可见的,但它们在派生类对象的内存布局中仍然存在,只是不可直接访问)。在C++等语言中,继承还涉及虚函数表的继承。每个包含虚函数的类都有一个虚函数表,该表存储了类中所有虚函数的地址。派生类继承基类时,会继承基类的虚函数表,并可能根据需要对某些虚函数进行重写。

(2) 虚函数重写(动态绑定/后期绑定):虚函数是C++等语言中实现多态的关键机制。虚函数允许在运行时(而不是编译时)根据对象的实际类型来确定调用哪个函数。当派生类重写了基类的虚函数时,派生类对象的虚函数表中对应虚函数的地址会被更新为派生类实现的地址。这样,当通过基类指针或引用指向派生类对象并调用虚函数时,实际调用的是派生类实现的版本。虚函数重写是多态性的核心,它使得相同的函数调用可以根据对象的实际类型产生不同的行为。

综上所述,要实现多态,必须满足两个条件:一是通过继承(或基类与派生类的关系)建立类之间的层次关系;二是在派生类中重写基类的虚函数,以实现动态绑定(在运行时根据对象的实际类型调用正确的方法)。这两个条件共同作用,使得程序能够根据对象的实际类型来调用相应的方法,从而实现多态性。

5.单继承和多继承关系的虚函数表

5.1 单继承中的虚函数表

我们来看看以下代码:

class Base 
{
public:
	virtual void func1() 
	{ 
		cout << "Base::func1" << endl; 
	}
	virtual void func2() 
	{ 
		cout << "Base::func2" << endl; 
	}
private:
	int a;
};
class Derive :public Base 
{
public:
	virtual void func1() 
	{ 
		cout << "Derive::func1" << endl; 
	}
	virtual void func3() 
	{ 
		cout << "Derive::func3" << endl; 
	}
	virtual void func4() 
	{ 
		cout << "Derive::func4" << endl; 
	}
private:
	int b;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

通过调试来观察一下:

我们可以发现:监视窗口并没有显示fun3和fun4。我们要如何查看d的虚表呢?我们可以通过下面的代码实现需求:

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()
{
	Base b;
	Derive d;
	// 思路:取出b、d对象的头8bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
	// 指针的指针数组,这个数组最后面放了一个nullptr
		// 1.先取b的地址,强转成一个int*的指针
		// 2.再解引用取值,就取到了b对象头8bytes的值,这个值就是指向虚表的指针
		// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
		// 4.虚表指针传递给PrintVTable进行打印虚表
		// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
	// 后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再
	// 编译就好了。
	// 这里我使用了reinterpret_cast来进行类型转换,这是更安全的做法,
	// 因为它明确指出了我们正在执行低级别的、可能依赖于实现的转换。
	VFPTR* vTableb = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&b));
	PrintVTable(vTableb);

	VFPTR* vTabled = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&d));
	PrintVTable(vTabled);
	return 0;
}

输出结果为:

5.2 多继承中的虚函数表

我们来了解多继承的虚函数表,来看看这段代码:

class Base1 
{
public:
	virtual void func1() 
	{ 
		cout << "Base1::func1" << endl; 
	}
	virtual void func2() 
	{ 
		cout << "Base1::func2" << endl; 
	}
private:
	int b1;
};

class Base2 
{
public:
	virtual void func1() 
	{ 
		cout << "Base2::func1" << endl; 
	}
	virtual void func2() 
	{ 
		cout << "Base2::func2" << endl; 
	}
private:
	int b2;
};

class Derive : public Base1, public Base2 
{
public:
	virtual void func1() 
	{ 
		cout << "Derive::func1" << endl; 
	}
	virtual void func3() 
	{ 
		cout << "Derive::func3" << endl; 
	}
private:
	int d1;
};

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()
{
	Derive d;
	VFPTR* vTableb1 = reinterpret_cast<VFPTR*>(*reinterpret_cast<intptr_t*>(&d));
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = *reinterpret_cast<VFPTR**>(reinterpret_cast<char*>(&d) + sizeof(Base1));
	PrintVTable(vTableb2);
	return 0;
}

输出结果为:

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

结束语

把有关多态的一些基础内容写了一下。

感谢各位大佬的支持!!!

希望这篇文章对您理解C++多态有所帮助!!!

求点赞收藏评论关注!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【CSS in Depth 2 精译_061】9.4 CSS 中的模式库 + 9.5 本章小结

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 【第九章 CSS 的模块化与作用域】 ✔️ 9.1 模块的定义 9.1.1 模块和全局样式9.1.2 一个简单的 CSS 模块9.1.3 模块的变体9.1.4 多元素模块 9.2 将模块组合为更大的结构 9.2.1 模块中多个职责的拆分…

DHCP服务(包含配置过程)

目录 一、 DHCP的定义 二、 使用DHCP的好处 三、 DHCP的分配方式 四、 DHCP的租约过程 1. 客户机请求IP 2. 服务器响应 3. 客户机选择IP 4. 服务器确定租约 5. 重新登录 6. 更新租约 五、 DHCP服务配置过程 一、 DHCP的定义 DHCP&#xff08;Dynamic Host Configur…

技术实践 | AI 安全:通过大模型解决高危WEB应用识别问题

一、引言 在日常企业安全能力建设中&#xff0c;收敛企业外网高危资产&#xff0c;以保障公司外部安全是企业安全的重要工作。WEB 高危服务&#xff08;如&#xff1a;管理后台、内部系统等&#xff09;外开是企业所面临的一个重要风险。针对该风险&#xff0c;传统的方式是基…

C 语言函数递归探秘:从基础概念到复杂问题求解的进阶之路

我的个人主页 我的专栏&#xff1a;C语言&#xff0c;希望能帮助到大家&#xff01;&#xff01;&#xff01;点赞❤ 收藏❤ 目录 什么是函数递归递归的基本组成递归的工作原理递归的优缺点递归的经典案例 5.1 阶乘计算5.2 斐波那契数列5.3 汉诺塔问题5.4 二分查找 递归的高级…

多输入多输出 | Matlab实现TCN-LSTM时间卷积神经网络结合长短期记忆神经网络多输入多输出预测

多输入多输出 | Matlab实现TCN-LSTM时间卷积神经网络结合长短期记忆神经网络多输入多输出预测 目录 多输入多输出 | Matlab实现TCN-LSTM时间卷积神经网络结合长短期记忆神经网络多输入多输出预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 多输入多输出 | Matlab实现…

「Mac畅玩鸿蒙与硬件33」UI互动应用篇10 - 数字猜谜游戏

本篇将带你实现一个简单的数字猜谜游戏。用户输入一个数字&#xff0c;应用会判断是否接近目标数字&#xff0c;并提供提示“高一点”或“低一点”&#xff0c;直到用户猜中目标数字。这个小游戏结合状态管理和用户交互&#xff0c;是一个入门级的互动应用示例。 关键词 UI互…

el-table根据接口返回某一个字段合并行

根据名称相同合并行 <template><div><el-table :data"responseSearchIntegralAddData" :span-method"objectSpanMethod1" border style"width: 100%"><el-table-column prop"integralTypeName" label"名称…

Linux系统之fuser命令的基本使用

Linux系统之fuser命令的基本使用 一、fuser命令介绍二、fuser命令使用帮助2.1 help帮助信息2.1 基本语法①通用选项②文件/设备相关选项③网络相关选项④进程操作选项⑤其他选项 三、fuser命令的基本使用3.1 查找挂载点的进程3.2 查看指定设备进程信息3.3 查找监听特定端口的进…

守护进程

目录 守护进程 前台进程 后台进程 session&#xff08;进程会话&#xff09; 前台任务和后台任务比较好 本质 绘画和终端都关掉了&#xff0c;那些任务仍然在 bash也退了&#xff0c;然后就托孤了 ​编辑 守护进程化---不想受到任何用户登陆和注销的影响​编辑 如何…

网络安全在现代企业中的重要作用

网络安全是这个数字时代最令人担忧的事情之一。对技术的依赖性越来越强&#xff0c;使其同时面临多种网络威胁。其声誉和法律后果的大幅下降可能归因于一次妥协。 这使得良好的网络安全成为所有企业的选择和必需品。本文介绍了网络安全的重要性、企业中常见的网络威胁以及公司…

C++学习日记---第14天(蓝桥杯备赛)

笔记复习 1.对象的初始化和清理 对象的初始化和清理是两个非常重要的安全问题&#xff0c;一个对象或者变量没有初始状态&#xff0c;对其使用后果是未知&#xff0c;同样的使用完一个对象或者变量&#xff0c;没有及时清理&#xff0c;也会造成一定的安全问题 构造函数&…

Kotlin DSL Gradle 指南

本文是关于 Kotlin DSL Gradle 的指南&#xff08;上篇&#xff09;&#xff0c;介绍了 Gradle 作为 Android 开发构建工具的作用及优势&#xff0c;包括初始配置、生命周期、依赖管理、Task 相关内容。如 Task 的创建、自定义、各种方法和属性&#xff0c;以及文件操作等&…

深度学习笔记之BERT(三)RoBERTa

深度学习笔记之RoBERTa 引言回顾&#xff1a;BERT的预训练策略RoBERTa训练过程分析静态掩码与动态掩码的比较模型输入模式与下一句预测使用大批量进行训练使用Byte-pair Encoding作为子词词元化算法更大的数据集和更多的训练步骤 RoBERTa配置 引言 本节将介绍一种基于 BERT \t…

扫振牙刷设计思路以及技术解析

市面上目前常见的就两种&#xff1a;扫振牙刷和超声波牙刷 为了防水&#xff0c;表面还涂上了一层防水漆 一开始的电池管理芯片&#xff0c;可以让充电更加均衡。 如TP4056 第一阶段以恒流充电&#xff1b;当电压达到预定值时转入第二阶段进行恒压充电&#xff0c;此时电流逐…

机器学习基础--基于常用分类算法实现手写数字识别

# 1.数据介绍 >MNIST 数据集来自美国国家标准与技术研究所, National Institute of Standards and Technology (NIST). 训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员. 测试集(test …

解决jupyter notebook 新建或打开.ipynb 报500 : Internal Server Error(涉及jinja2兼容性问题)

报错&#xff1a; [E 10:09:52.362 NotebookApp] 500 GET /notebooks/Untitled16.ipynb?kernel_namepyt hon3 (::1) 93.000000ms refererhttp://localhost:8888/tree ...... 重点是&#xff1a; from .exporters import * File "C:\ProgramData\Anaconda3\lib\site-p…

基于Springboot企业级工位管理系统【附源码】

基于Springboot企业级工位管理系统 效果如下&#xff1a; 系统登录页面 员工主页面 部门信息页面 员工管理页面 部门信息管理页面 工位信息管理页面 工位分配管理页面 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所。…

GoogleTest做单元测试

目录 环境准备GoogleTest 环境准备 git clone https://github.com/google/googletest.git说cmkae版本过低了&#xff0c;解决方法 进到googletest中 cmake CMakeLists.txt make sudo make installls /usr/local/lib存在以下文件说明安装成功 中间出了个问题就是&#xff0c;…

Android 11 三方应用监听关机广播ACTION_SHUTDOWN

前言 最近有项目过程中&#xff0c;有做app的同事反馈&#xff0c;三方应用无法监听关机广播。特地研究了下关机广播为啥监听不到。 1.原因&#xff1a;发送关机广播的类是ShutdownThread.java&#xff0c;添加了flag:Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER…

一篇文章了解Linux

目录 一&#xff1a;命令 1 ls命令作用 2 目录切换命令&#xff08;cd/pwd&#xff09; &#xff08;1)cd切换工作目录命令 3 相对路径、绝对路径和特殊路径 (1)相对路径和绝对路径的概念和写法 (2)几种特殊路径的表示符 (3)练习题&#xff1a; 4 创建目录命令&#x…