C++ 之 多态 【虚函数表、多态的原理、动态绑定与静态绑定】

news2025/7/20 19:02:49

目录

前言

1.多态的原理

1.1虚函数表

1.2派生类中的虚表

 1.3虚函数、虚表存放位置

 1.4多态的原理

1.5多态条件的思考 

2.动态绑定与静态绑定 

3.单继承和虚继承中的虚函数表 

3.1单继承中的虚函数表

 3.2多继承(非菱形继承)中的虚函数表

 4.问答题


前言

需要声明的,这期博客的代码及解释都是在vs202022下的x86程序中,涉及的指针都是4bytes。 如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题 等等

1.多态的原理

1.1虚函数表

这里常考一道笔试题:sizeof(Base)是多少

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

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

8字节说明了,对象中除了存储成员变量_b,还存储着些什么

有一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)

一个含有虚函数的类中都至少有一个虚函数表指针

因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

1.2派生类中的虚表

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)基类对象包含一个虚函数表指针(vptr),指向基类的虚函数表

派生类对象同样包含一个vptr,指向派生类的虚函数表,

在派生类对象的内存布局上,

派生类对象的虚表指针覆盖了所包含的基类子对象的虚表指针,指向的是派生类的虚函数表

即派生类的虚表指针存放到了所包含的基类子对象虚表指针的位置

(2)基类b对象和派生类d对象虚表是不一样的,

这里我们发现Func1完成了重写,所以d的虚表中存储的是重写的Derive::Func1

所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。

重写是语法层的叫法,覆盖是原理层的叫法

(3)另外Func2继承下来后是虚函数,所以放进了虚表

Func3也继承下来了,但不是虚函 数,所以不会放进虚表

(4)虚函数表本质是一个存虚函数指针的函数指针数组

一般情况这个数组最后面放了一个nullptr

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

a.派生类首先会继承基类的虚函数表结构

b.派生类重写了基类中的某个虚函数

就用派生类的虚函数地址覆盖虚表中基类的虚函数地址 

c.派生类自己新增加的虚函数按其在派生类中的声明次序追加到派生类虚表的最后

 

 1.3虚函数、虚表存放位置

虚函数与普通函数一样,存放在代码段中

虚函数表的存放位置由编译器决定,不同的编译器可能有不同的实现方式

下面是在vs2022 X86环境下运行的代码及结果,推断虚表存放在常量区(只读数据段)

//验证虚表存放在常量区(代码段)
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	char _b = 1;
};

int main()
{
	int a = 10;
	printf("栈:%p\n", &a);

	int* b = new int(5);
	printf("堆:%p\n", b);

	static int c = 20;
	printf("静态区:%p\n", &c);

	const char* p = "ncoa";
	printf("常量区:%p\n", p);

	Base b;
	printf("虚表地址:%p\n", *((int*)&b));
	return 0;
}

所以同一个类的对象共用同一张虚表 

 1.4多态的原理

 

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
protected: int a = 10;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl;}
protected: int b = 20;
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);

	return 0;
}

 (1)观察下图的红色箭头我们看到,

p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数Person::BuyTicket

(2)观察下图的蓝色箭头我们看到

p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数Student::BuyTicket

这样就实现出了不同对象去完成同一行为时,展现出不同的形态

多态的原理就是根据指向的对象类型使用该对象的虚表指针找到到该对象的虚表 然后再在虚表中去找到并调用相应函数(如果有)

1.5多态条件的思考 

(1)多态要求必须通过父类的指针或引用实现:

因为父类的指针既可以指向父类对象也可以指向子类对象

指向父类对象时就在父类对象的虚表中去找到相应函数并调用(如果有)

指向子类对象时就在子类对象的虚表中去找到相应函数并调用(如果有)

同理父类的引用可以是父类对象的别名也可以是子类对象的别名

是父类对象的别名时就在父类对象的虚表中去找到相应函数并调用(如果有)

是子类对象的别名时就在子类对象的虚表中去找到相应函数并调用(如果有)

不能是父类对象的原因是:

赋值切片时,虚表并不进行拷贝,此时通过父类对象只能调用父类的虚函数

假设虚表要进行拷贝,那么会导致父类对象调用的虚函数不明确

(既可能是父类对象的虚函数也可能是子类对象的虚函数)

 (2)多态要求实现虚函数的重写:
只有实现了虚函数的重写,才能实现相同函数名调用时出现不同的形态

2.动态绑定与静态绑定 

class person {
public:
	virtual void buyticket() 
	{ 
		cout << "买票-全价" << endl;
	}
protected:
	int a = 10;
};
class student : public person {
public:
	virtual void buyticket() 
	{ 
		cout << "买票-半价" << endl;
	}
protected:
	int b = 20;
};
void func(person* p)
{
	p->buyticket();
}
int main()
{
	person mike;
	func(&mike);

	mike.buyticket();
	return 0;
}

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的 

不满足多态的函数调用,在编译时确定

1.静态绑定又称为前期绑定(早绑定)

是指在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载、模板等

2. 动态绑定又称后期绑定(晚绑定)

是指在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,

也称为动态多态,比如:构成多态条件的虚函数的调用等

3.单继承和虚继承中的虚函数表 

3.1单继承中的虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a = 10;
};
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 = 20;
};

int main()
{
	Base b1;
	Derive d1;

	return 0;
}

通过监视窗口,我们发现d1虚表当中似乎没有追加自身虚函数的地址

 但通过内存窗口,我们可以看到虚表中存放有四个指针,我们打印出来看看

接上面的代码
//给函数指针起个别名
typedef void(*FUNC_VFT)();

void Print(FUNC_VFT* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		FUNC_VFT f = table[i];
		f();
	}
	printf("\n");
}
int main()
{
	Base b1;
	Derive d1;
	//取到虚表指针,虚表指针本质是一个函数二级指针
	FUNC_VFT* table = (FUNC_VFT*)(*((int*)&d1));

	Print(table);

	return 0;
}

我们看到,派生类的虚表确实在末尾追加了自己定义的虚函数地址 

关于上面验证的细节:

(1) 认识到虚表本质上是一个函数指针数组,虚表指针就是一个函数二级指针

(1) 函数指针看起来比较复杂,起个别名更简便

(1) 对象的前四个字节存放的是虚表指针(不同平台不一样)

(1) for循环中,虚表末尾通常放一个nullptr(不同平台可能不一样,此时则需要定死循环次数)

同时,在vs下,你在编译好的情况下再次修改代码,nullptr可能就会消失,这时候只需要重写生成解决方案即可

(1) 关于函数调用,这里的情况非常特殊,成员函数没有参数,且没有调用this指针

不然的话就会大坑

(1) 当然,调用的虚函数打印的话也在此次验证中起到了举足轻重的作用

 3.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;
	
	return 0;
}

对象d分别继承两个基类的虚表结构,完成虚函数的覆盖

只是同样看不见自己的func3存放到哪里,打印出来看看

int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	//VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	Base2* p = &d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)p);
	PrintVTable(vTableb2);
	return 0;
}

存放到了Base1的虚表当中

结论就是:多继承派生类自身的虚函数追加第一个继承基类部分的虚函数表末尾

这保持了与单继承的一致性

简化了多态调用的实现(第一个基类指针直接调用无需偏移量调整)

调用d对象中的func1函数

 两张虚表当中都有func1函数,但fun1函数的地址只有一个

编译器针对func1函数的调用,首先需要传this指针,其次call 函数func1的地址

子类包含的第一个父类子对象的地址与派生类对象的地址相同,

使用第一个父类指针调用func1时,传完该指针之后可以直接调用func1

子类类包含的第一个父类子对象的地址与派生类对象的地址相同,

使用第二个父类指针调用func1时,传完该指针之后需修正使其指向派生类对象的地址,最后进行调用

 4.问答题

4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。

8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

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

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

相关文章

【JavaWeb】Maven、Servlet、cookie/session

目录 5. Maven6. Servlet6.1 Servlet 简介6.2 HelloServlet6.3 Servlet原理6.4 Mapping( **<font style"color:rgb(44, 44, 54);">映射 ** )问题6.5 ServletContext6.6 HttpServletResponse<font style"color:rgb(232, 62, 140);background-color:rgb(…

Rust 编程实现猜数字游戏

文章目录 编程实现猜数字游戏游戏规则创建新项目默认代码处理用户输入代码解析 生成随机数添加依赖生成逻辑 比较猜测值与目标值类型转换 循环与错误处理优化添加循环优雅处理非法输入​ 最终完整代码核心概念总结 编程实现猜数字游戏 我们使用cargo和rust实现一个经典编程练习…

关于神经网络中的激活函数

这篇博客主要介绍一下神经网络中的激活函数以及为什么要存在激活函数。 首先&#xff0c;我先做一个简单的类比&#xff1a;激活函数的作用就像给神经网络里的 “数字信号” 加了一个 “智能阀门”&#xff0c;让机器能学会像人类一样思考复杂问题。 没有激活i函数的神经网络…

CentOS_7.9 2U物理服务器上部署系统简易操作步骤

近期单位网站革新&#xff0c;鉴于安全加固&#xff0c;计划将原有Windows环境更新到Linux-CentOS 7.9&#xff0c;这版本也没的说&#xff08;绝&#xff09;了&#xff08;版&#xff09;官方停止更新&#xff0c;但无论如何还是被sisi的牵挂着这一大批人&#xff0c;毕竟从接…

短视频平台差异视角下开源AI智能名片链动2+1模式S2B2C商城小程序的适配性研究——以抖音与快手为例

摘要 本文以抖音与快手两大短视频平台为研究对象&#xff0c;从用户群体、内容生态、推荐逻辑三维度分析其差异化特征&#xff0c;并探讨开源AI智能名片链动21模式与S2B2C商城小程序在平台适配中的创新价值。研究发现&#xff0c;抖音的流量中心化机制与优质内容导向适合品牌化…

【笔记】Windows 下载并安装 ChromeDriver

以下是 在 Windows 上下载并安装 ChromeDriver 的笔记&#xff1a; ✅ Windows 下载并安装 ChromeDriver 1️⃣ 确认 Chrome 浏览器版本 打开 Chrome 浏览器 点击右上角 ︙ → 帮助 → 关于 Google Chrome 记下版本号&#xff0c;例如&#xff1a;114.0.5735.199 2️⃣ 下载…

Spark-Core Project

RDD转换算子总结 RDD转换算子分为Value类型、双Value类型和Key - Value类型。 1、Value类型 map&#xff1a;对数据逐条映射转换&#xff0c;可改变数据类型或值。如 dataRDD.map(num > num * 2 运行结果&#xff1a; 2&#xff09;mapPartitions&#xff1a;以分区为单位处…

Wireshark 使用教程:让抓包不再神秘

一、什么是 tshark&#xff1f; tshark 是 Wireshark 的命令行版本&#xff0c;支持几乎所有 Wireshark 的核心功能。它可以用来&#xff1a; 抓包并保存为 pcap 文件 实时显示数据包信息 提取指定字段进行分析 配合 shell 脚本完成自动化任务 二、安装与验证 Kali Linux…

JWT安全:接收无签名令牌.【签名算法设置为none绕过验证】

JWT安全&#xff1a;假密钥【签名随便写实现越权绕过.】 JSON Web 令牌 (JWT)是一种在系统之间发送加密签名 JSON 数据的标准化格式。理论上&#xff0c;它们可以包含任何类型的数据&#xff0c;但最常用于在身份验证、会话处理和访问控制机制中发送有关用户的信息(“声明”)。…

白银价格查询接口如何用Java进行调用?

一、什么是白银价格查询接口&#xff1f; 它聚焦于上海黄金交易所、上海期货交易所等权威市场&#xff0c;精准提供白银价格行情数据&#xff0c;助力用户实时把握市场脉搏&#xff0c;做出明智的投资决策。 二、应用场景 分析软件&#xff1a;金融类平台可以集成本接口&…

FreeBSD 14.3 候选版本附带 Docker 镜像和关键修复

新的月份已经到来&#xff0c;FreeBSD 14.3 候选发布版 1 现已开放测试&#xff0c;它带来了一些您可能会觉得有用的更新&#xff0c;特别是如果您对Docker容器感兴趣的话。RC1 版本中一个非常受欢迎的改进是&#xff0c;FreeBSD 项目已开始将官方开放容器计划 (OCI) 镜像发布到…

「Java教案」算术运算符与表达式

课程目标 1&#xff0e;知识目标 能够区分Java运算符的种类&#xff0c;例如&#xff0c;算术、赋值、关系、逻辑、位运算等。能够区分Java各类运算符的功能和使用场景。能够根据表达式的构成和计算规则&#xff0c;写出正确的表达式。能够根据运算符优先级与结合性&#xff…

论文写作核心要点

不要只读论文里的motivation和method 论文里的图表和统计特征 在论文里找到具有统计意义的东西&#xff0c;那么在语料里也肯定遵循这样的规律&#xff0c;我们就能用机器学习的方法&#xff0c; 我们再用不同方法解决&#xff0c;哪种方法好&#xff0c;就用哪种 实验分析 …

[java]eclipse中windowbuilder插件在线安装

目录 一、打开eclipse 二、打开插件市场 三、输入windowbuilder&#xff0c;点击install 四、进入安装界面 五、勾选我同意... 重启即可 一、打开eclipse 二、打开插件市场 三、输入windowbuilder&#xff0c;点击install 四、进入安装界面 五、勾选我同意... 重启即可

Ubuntu20.04服务器开启路由转发让局域网内其他电脑通过该服务器连接外网

要让你的 Ubuntu作为路由器&#xff0c;通过 Wi-Fi 上网&#xff0c;并给连接到 UsbNetwork 的设备提供网络&#xff0c;需要做以下配置&#xff1a; 1. 网络拓扑 [互联网] ← (Wi-Fi, wlo1) → [Ubuntu] ← (USB网络/USB以太网, UsbNetwork) → [设备]Ubuntu&#xff1a; Wi-…

【Oracle】TCL语言

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. TCL概述1.1 什么是TCL&#xff1f;1.2 TCL的核心功能 2. 事务基础概念2.1 事务的ACID特性2.2 事务的生命周期 3. COMMIT语句详解3.1 COMMIT基础语法3.2 自动提交与手动提交3.3 提交性能优化 4. ROLLBACK语句…

Windows | 总误按Num Lock?修改注册表永久禁用Numlk键使小键盘一直输入数字

先说需修改注册表的位置与键值 路径&#xff1a;HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout\ 二进制键&#xff1a;Scancode Map 键值&#xff1a; 00 00 00 00 00 00 00 00 01 00 00 00 00 00 45 00 00 00 00 00 00 00 00 00如下图&#xff1a; …

2.RV1126-OPENCV Mat理解和AT函数

一.Mat概念 Mat 是整个图像存储的核心也是所有图像处理的最基础的类&#xff0c;Mat 主要存储图像的矩阵类型&#xff0c;包括向量、矩阵、灰度或者彩色图像等等。Mat由两部分组成&#xff1a;矩阵头&#xff0c;矩阵数据。矩阵头是存储图像的长度、宽度、色彩信息等头部信息&a…

unidbg patch 初探 微博deviceId 案例

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向过程 看了b站迷人瑞信那个由于是…

循序渐进 Android Binder(一):IPC 基本概念和 AIDL 跨进程通信的简单实例

Binder 给人的第一印象是”捆绑者“&#xff0c;即将两个需要建立关系的事物用某些工具束缚在一起。在 Android 中&#xff0c;Binder 是一种高效的跨进程通信&#xff08;IPC&#xff09;机制&#xff0c;它将可以将运行在不同进程中的组件进行绑定&#xff0c;以实现彼此通信…