C++ 多态之虚函数表

news2025/7/11 5:29:48

虚函数表概述

C++ 的多态,使用动态绑定的技术,技术的核心是虚函数表(简称虚表),每个包含了虚函数的类都包含一个虚表,虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象共用一个虚表。

1.虚表指针

每个包含虚函数的类,都有一个虚表指针,虚表指针在数据结构的第一个,对象在创建时就有了这个指针,并且指向了该类的虚表。

#include <iostream>
using namespace std;
class TBase {
	void fun1(){};
	void fun2(){};
	void fun3(){};
	int  x;
};
class VTBase {
	virtual void fun1(){};
	virtual void fun2(){};
	virtual void fun3(){};
	int          x;
};
int main()
{
	TBase tbase;
	VTBase vtbase;
	std::cout << sizeof(tbase) << endl;
	std::cout << sizeof(vtbase) << endl;    
}
  • 如果是x64编译,默认是8字节对齐 输出为4,16
  • 如果是x86编译,默认是4字节对齐 输出为4,8

我们知道函数是不会占用类的空间的,所以第一个Tbase类大小始终为4。含有虚函数的类,无论有多少个虚函数,都会增加一个虚表指针的大小(x64:8,x86:4)

另外可以通过offsetof来看偏移,以x64编译器为例子:

cout << offsetof(VTBase, x) << endl;//输出值为8

x偏移为8,是我们结构体中可见的首个变量,所以前8个字节为虚函数表指针的大小,也是该类的第一个成员变量。

在这里插入图片描述

1.1剖析虚表指针的调用

这里还是以X64编译器举例

  • 为了方便看代码,我们首先
    typedef long long Vtpr_type;
    typedef void (*VFunc)(void); //定义一个函数指针类型变量类型 VFunc
  • 还是以VTBase类举例子,我们已知前8个字节为虚函数表指针,所以通过变量指向该地址
	VTBase* v_base = new VTBase();
	//将对象的首地址输出
	Vtpr_type* vptr = nullptr;
	//vptr是虚函数类的第一个成员变量,也是虚函数表的指针
	memcpy(&vptr, v_base, sizeof(Vtpr_type));
  • 虚函数的地址存储在了虚函数表中,虚函数表地址内的值,为虚函数的指针,我们依次获取虚函数表中的值,并执行
	VFunc funa = (VFunc)(*vptr);
	VFunc funb = (VFunc)(*(vptr + 1));
	VFunc func = (VFunc)(*(vptr + 2));
	funa();//VTBase::a()
	funb();//VTBase::b()
	func();//VTBase::c()

完整代码

#include <iostream>
using namespace std;
namespace {
	class VTBase {
	public:
		virtual void a() { cout << "VTBase::a()" << endl; }
		virtual void b() { cout << "VTBase::b()" << endl; }
		virtual void c() { cout << "VTBase::c()" << endl; }
		int          x, y;
	};

	typedef long long Vtpr_type;
	typedef void (*VFunc)(void); //定义一个函数指针类型变量类型 VFunc

} // namespace

int main()
{
	//offsetof
	VTBase vtbase;
	std::cout << sizeof(vtbase) << endl;
	cout << offsetof(VTBase, x) << endl;
	cout << offsetof(VTBase, y) << endl;
	/*
	x偏移为8,y偏移为12
	所以前8个字节为虚函数表的大小,也是该类的第一个成员变量
	*/
	VTBase* v_base = new VTBase();
	//将对象的首地址输出
	Vtpr_type* vptr = nullptr;
	//vptr是虚函数类的第一个成员变量,也是虚函数表的指针
	memcpy(&vptr, v_base, sizeof(Vtpr_type));
	//虚函数的地址存储在了虚函数表中,虚函数表地址内的值,为虚函数的指针
	VFunc funa = (VFunc)(*vptr);
	VFunc funb = (VFunc)(*(vptr + 1));
	VFunc func = (VFunc)(*(vptr + 2));
	funa();
	funb();
	func();
	delete v_base;
	v_base = nullptr;
	return 0;
}

1.2多个类共用一份虚函数表释义

void TestVtprValue() {
		VTBase* v_base1 = new VTBase();
		VTBase* v_base2 = new VTBase();
		Vtpr_type* vptr1   = nullptr;
		Vtpr_type* vptr2   = nullptr;
		memcpy(&vptr1, v_base1, sizeof(Vtpr_type));
		memcpy(&vptr2, v_base2, sizeof(Vtpr_type));
		cout << vptr1 << endl;
		cout << vptr2 << endl;
	}

在这里插入图片描述

2.继承中的虚函数表

我们先定义一个派生类VTBaseExt,并且重写其中某一个虚函数

class VTBaseExt : public VTBase {
	public:
		virtual void b() { cout << "VTBaseExt::b()" << endl; }
};
void TestVtbExt()
{
	VTBase*    v_base    = new VTBase();
	Vtpr_type* vptr_base = nullptr;
	memcpy(&vptr_base, v_base, sizeof(Vtpr_type));
	cout << vptr_base << endl;

	VTBaseExt* v_base_ext = new VTBaseExt();
	Vtpr_type* vptr_ext   = nullptr;
	memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));
	cout << vptr_ext << endl;
		
	VFunc funa = (VFunc)(*(vptr_ext));
	VFunc funb = (VFunc)(*(vptr_ext + 1));
	VFunc func = (VFunc)(*(vptr_ext + 2));
	funa();//VTBase::a()
	funb();//VTBaseExt::b()
	func();//VTBase::c()
}

此时执行,我们发现VTBase、VTBaseExt虚表大小均为4,但虚表数组中储存的第二个值发生了变化
在这里插入图片描述

2.1剖析派生类调用基类的虚函数是否为同一个

从上图可知是同一个
在这里插入图片描述
获取虚函数表数组存储的函数地址

void TestVtbExt()
{
	VTBase*    v_base    = new VTBase();
	Vtpr_type* vptr_base = nullptr;
	memcpy(&vptr_base, v_base, sizeof(Vtpr_type));
	cout << vptr_base << endl;

	VTBaseExt* v_base_ext = new VTBaseExt();
	Vtpr_type* vptr_ext   = nullptr;
	memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));
	cout << vptr_ext << endl;

	//获取虚函数表数组地址
	for (int i = 0; i < 3; i++) {
		cout << *(vptr_base + i) << " " << *(vptr_ext + i) << endl;
	}
}

从图中可以看出来,只有被重写过的第二组是不一样的,其余的函数地址都没有变化
在这里插入图片描述

2.2基类指针指向派生类的虚函数表

调用代码释义:

void TestVtbPointer() {
	//基类指针指向自己
	VTBase*    v_base    = new VTBase();
	Vtpr_type* vptr_base = nullptr;
	memcpy(&vptr_base, v_base, sizeof(Vtpr_type));
	cout << vptr_base << endl;
	//派生类指针指向派生类
	VTBaseExt* v_base_ext = new VTBaseExt();
	Vtpr_type* vptr_ext   = nullptr;
	memcpy(&vptr_ext, v_base_ext, sizeof(Vtpr_type));
	cout << vptr_ext << endl;
	//基类指针指向派生类
	VTBase*    v_base_to_ext = new VTBaseExt();
	Vtpr_type* vptr_to_ext   = nullptr;
	memcpy(&vptr_to_ext, v_base_to_ext, sizeof(Vtpr_type));
	cout << vptr_to_ext << endl;
}

从结果输出来看,最终的基类指针指向派生类对象,虚函数表的指针还是指向了派生类的虚函数表
在这里插入图片描述

3.虚函数表总结

动态绑定的最终实现就是查虚函数表,每个对象的类对应了自己的虚函数表,虚函数表存储了与之对应的虚函数地址。

3.1动态绑定示意图

  • 假设含有虚函数的基类VTBase ;
  • VTBase 派生类VTBaseExt,重写了func_2;
  • VTBaseExt派生类VTBaseFinal ,新增了func_4;
class VTBase {
public:
	virtual void func_1() { cout << __FUNCTION__ << endl; }
	virtual void func_2() { cout << __FUNCTION__ << endl; }
	virtual void func_3() { cout << __FUNCTION__ << endl; }
	int          x;
};
class VTBaseExt : public VTBase {
public:
	virtual void func_2() { cout << "VTBaseExt:: " << __FUNCTION__ << endl; }
};
class VTBaseFinal : public VTBaseExt {
public:
	virtual void func_4() { cout << "VTBaseFinal:: " << __FUNCTION__ << endl; }
};

在这里插入图片描述

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

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

相关文章

猴子也能学会的jQuery第十期——jQuery元素操作(下)

&#x1f4da;系列文章—目录&#x1f525; 猴子也能学会的jQuery第一期——什么是jQuery 猴子也能学会的jQuery第二期——引用jQuery 猴子也能学会的jQuery第三期——使用jQuery 猴子也能学会的jQuery第四期——jQuery选择器大全 猴子也能学会的jQuery第五期——jQuery样式操作…

树莓派系统安装,使用SSD/U盘启动centos

树莓派系统安装&#xff0c;使用SSD/U盘启动centos argon m2 外壳厂家资料 https://www.waveshare.net/wiki/PI4-CASE-ARGON-ONE-M.2 TF卡安装系统 如果是使用TF卡安装&#xff0c;则参考官方文档按步骤安装即可&#xff1a; https://www.waveshare.net/wiki/Raspberry_Pi_Do…

FAlphaBlend——Unreal中的插值助手

游戏开发中经常要进行插值处理&#xff0c;这个东西处理虽然不复杂&#xff0c;但如果自己重新做&#xff0c;还是要写额外的代码&#xff0c;好消息是&#xff0c;Unreal已经为我们提供了插值助手——FAlphaBlend。 我们以一个非常简单的应用场景来说明FAlphaBlend的用处&…

matplotlib详细教学

Matplotlib初相识 认识matplotlib Matplotlib是一个Python 2D绘图库&#xff0c;能够以多种硬拷贝格式和跨平台的交互式环境生成出版物质量的图形&#xff0c;用来绘制各种静态&#xff0c;动态&#xff0c;交互式的图表 一个最简单的绘图例子 matplotlib的图像都是画在对应…

Spring事务与事务传播机制

目录 1.事务的基本概念 2.Spring事务的实现 3.事务隔离级别 4.事务传播机制 1.事务的基本概念 关于事务的一些基础概念我已经在MYSQL中讲解过了&#xff0c;有不了解的可以移步至此篇文章&#xff1a;MySQL基础——数据库索引与事务_invictusQAQ的博客-CSDN博客 2.Spring…

基于AlexNet卷积神经网络的手写体数字倾斜校正系统研究-附Matlab代码

⭕⭕ 目 录 ⭕⭕✳️ 一、引言✳️ 二、AlexNet 网络✳️ 三、实验验证✳️ 3.1 实验数据集✳️ 3.2 数据训练✳️ 3.3 手写体倾斜数字校正结果✳️ 四、参考文献✳️ 五、Matlab代码获取✳️ 一、引言 手写体数字识别是光学字符识别(Optical Character&#xff32;ecognition…

破圈的《张朝阳的物理课》,开启“知识突围”的搜狐视频

在互联网耕耘二十多年的搜狐&#xff0c;正在焕发出新的活力。 从搜狐最近公布的2022年第三季度财报来看&#xff0c;营收1.85亿美元&#xff0c;其中在线游戏业务实现收入1.49亿美元&#xff0c;广告收入环比增长3%达到2600万美元。同时&#xff0c;净亏损好于此前预期。 对…

Junit执行源码分析,junit是怎么跑起来的(二)

接上一篇【Junit执行源码分析&#xff0c;junit是怎么跑起来的】 https://blog.csdn.net/Aqu415/article/details/127494898 这里我们接着分析 org.junit.runner.JUnitCore#run(org.junit.runner.Runner) 这个方法 public Result run(Runner runner) {Result result new Resu…

智慧监狱解决方案-最新全套文件

智慧监狱解决方案-最新全套文件一、建设背景二、思路架构三、建设方案四、获取 - 智慧监狱全套最新解决方案合集一、建设背景 “智慧监狱”是“数字法治、智慧司法”信息系统建设的重要组成部分&#xff0c;其主要内容是在现有监狱信息建设的基础上&#xff0c;充分利用大数据…

【机器学习】拉格朗日对偶性

有任何的书写错误、排版错误、概念错误等&#xff0c;希望大家包含指正。 拉格朗日对偶性 在求解最优化问题中&#xff0c;拉格朗日乘数法&#xff08;Lagrange Multiplier&#xff09;和 KKT&#xff08;Karush Kuhn Tucker&#xff0c;三个人名&#xff09;条件是两种最常用…

高性能零售IT系统的建设07-通过一次重大危机感受Redis从使用到失智到理性的治理

介绍 在2020年年初我接手的一座“屎山”里含有Redis框架和机制&#xff0c;它使用的是sentinel模式。其实sentinel模式并不是重点&#xff0c;按照我的经验&#xff0c;每天单店10万单也一样可以使用Redis Sentinel。只有到达新浪微博啦、头条啦这种大厂才有必要去架设redis cl…

Linux进程替换

进程替换 假如操作系统正在执行某一个程序&#xff0c;我们可以利用程序替换函数指定一个新的程序&#xff0c;让操作系统去执行我们新指定的程序。也就是这样一种情形下&#xff0c;我们fork一个进程&#xff0c;如果fork成功&#xff0c;子进程会和父进程执行相同的代码&…

基于springboot+vue的社区健康码管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

Hive数据操纵语言-DML(Load、insert、事务表)

1. Load加载数据 1.1 概述 主要为将数据文件移动到Hive表对应的位置&#xff0c;即复制、移动操作 1.2 语法 1.2.1 filepath 表示待移动数据的路径文件路径支持下面三种形式&#xff0c;要结合LOCAL关键字一起考虑&#xff1a; 相对路径&#xff0c;例如&#xff1a;projec…

音乐播放

在Qt5中使用Qt Multimedia 模块来实现多媒体应用&#xff08;音视频播放和控制&#xff0c;相机拍照。收音等&#xff09;。 使用多媒体模块时需要在pro文件中添加&#xff1a; QT multimedia QMediaPlayer&#xff08;播放音频&#xff09; 不追求低延迟的话使用QMediaPlaye…

作业练习3:类的继承

作业练习3&#xff1a;类的继承 面向对象程序设计(C) WHUT-CS 2022 Spring 源码传送门 传送门&#xff1a;https://pan.baidu.com/s/11KwE6tQzC_H-31AFgEWtOg?pwd1111 I.作业目的 本次实验主要在于学习使用C类继承机制实现程序功能。C中的举继承机制能够用于表示类之间的…

Crack:wodXMPP ActiveX 即时通讯组件

wodXMPP ActiveX 组件 XMPP组件&#xff0c;Jabber(ICQ MSN AIM Yahoo GTalk)即时通讯组件 wodXMPP 是 XMPP/Jabber&#xff08;可扩展消息传递和状态协议&#xff09;协议的客户端组件。它用于创建轻量级的消息传递客户端&#xff0c;并且除了 wodXMPP 之外不需要其他第 3 方要…

第八章《Java高级语法》第10节:注解

注解可以被理解为一种特殊的注释。普通注释是添加到代码中的人类语言,它可以提高程序的可读性。当源程序被编译为字节码之后,普通注释都会被去除掉,因为这些注释对代码的执行没有任何影响。因此,普通注释只能对代码的阅读者起到帮助。而注释则不同,注释可以对编译器和虚拟…

数字验证学习笔记——UVM学习1

一、类库地图 在SV模块中&#xff0c;验证环境整体的构建&#xff0c;是从底层模块的验证组件搭建到通信和激励生成这些元素无论是软件对象的创建、访问、修改、配置&#xff0c;还是组件之间的通信等都是通过用户自定义的方式来实现的。UVM验证方法学作为之前所有方法学的融合…

ubuntu22.04安装教程

1、选择语言 (默认) 2、取消安装更新 (默认) 3、选择键盘语言 (默认) 4、配置ip&#xff0c;可以直接选择dhcp&#xff0c;也可选择配置静态ip (默认) 5、配置代理 跳过不填写 6、设置镜像源 (默认) https://mirrors.aliyun.com/ubuntu/ 7、磁盘配置&#xff0c;默认即可 (默…