C++:多态的底层实现原理 -- 虚函数表

news2024/9/27 5:41:52

目录

一. 多态的原理

1.1 虚函数表

1.2 多态的实现原理

1.3 动态绑定与静态绑定

二. 多继承中的虚函数表

2.1 虚函数表的打印

2.2 多继承中虚函数表中的内容存储情况


一. 多态的原理

1.1 虚函数表

对于一个含有虚函数的的类,在实例化出来对象以后,对象所存储的内容包含两部分:

  • 类的成员变量。
  • 一个指向虚函数表得虚函数表指针。

下段代码定义了一个Base类,其中包含虚函数func1以及一个int型数据,在main函数中,使用sizeof(Base)计算这个类实例化出来的对象大小为8bytes而不是4bytes,这正是因为虚函数表指针占了4bytes的存储空间(32位编译环境)。

class Base
{
public:
	virtual void func() { std::cout << "Base::func()" << std::endl; }

	int _b = 1;
};

int main()
{
	Base b;
	std::cout << sizeof(b) << std::endl;  //8
	return 0;
}

如果要调用Base中定义的虚函数func,那么程序会在运行时根据虚函数指针找到虚函数表,虚函数表中存有函数指针(函数所在地址),程序会根据虚函数表中存储的虚函数所在地址,找到对应的函数进行调用。

图1.1 虚函数指针和虚函数表

1.2 多态的实现原理

多态的实现,是通过虚函数的重写来实现的。对于一个包含虚函数的基类Base,设有一派生类Derive继承了基类Base,那么Derive会将Base的虚函数表一并继承下来。

  • 如果Derive中没有对Base中的虚函数进行重写,那么Derive和Base各自拥有不同的虚函数表,两者虚函数表中存储的内容相同。
  • 如果Derive对Base的虚函数完成了重写,那么虚函数表中的被重写的虚函数的地址会被覆盖,更新为派生类中对应的虚函数地址。

演示代码1.2中定义了一个基类Base和一个派生类Derive,Base中定义了两个虚函数func1和func2,在派生类Derive中,func2被重写了,func1没有被重写。运行代码,打开内存监视窗口,可以看到,在Derive对象的虚函数表中,func2的地址和Base对象中的不一样,而func1的地址一样,这证明了func2被重写后,其记录在虚函数表中的地址被覆盖了。

演示代码1.2:

#include<iostream>

class Base
{
public:
	virtual void func1() 
	{ 
		std::cout << "Base::func1()" << std::endl; 
	}

	virtual void func2()
	{
		std::cout << "Base::func2()" << std::endl;
	}
};

class Derive : public Base
{
public:
	virtual void func2()
	{
		std::cout << "Derive::func2()" << std::endl;
	}

	int _d = 1;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}
图1.2 继承体系中的虚函数表及虚函数覆盖情况

我们知道,多态的条件之一,就是通过父类的指针或引用去调用,可以从这个调用条件为切入点,分析多态中函数调用的流程,来探索多态的底层原理,多态中函数调用流程为:

  1. 将父类或子类的对象(地址)赋值给父类的引用(指针)。
  2. 父类的对象或引用,根据其实际表示的对象或指向,拿到对应的虚表函数指针,在虚函数表中找到要调用的虚函数地址,来调用对应的函数。

正是由于子类对象中完成了对父类对象虚函数的重写,所以在子类对象完成虚函数操作时,会执行子类中定义的虚函数。多态中的虚函数调用,是通过获取虚函数表中的函数指针来确定具体调用哪个函数的,由于父类对象和子类对象的虚函数表中存储不同的虚函数指针,所以会调用不同的虚函数,从而实现了多态。

关于多态中虚函数表的生成和覆盖,总结出以下几点关键内容:

  • 虚函数表本质是一个存虚函数指针的指针数组,在VS编译环境下,这个数组最后面放了一个nullptr,但是在Linux gcc编译环境下,后面不会存有nullptr。
  • 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中  b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数  c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 虚函数表中存的是虚函数指针,而不是虚函数。虚函数和普通函数一样,存储在代码段,对象中存储的也不是虚函数表,而是虚函数表指针,其指向虚函数表所在的地址。

1.3 动态绑定与静态绑定

  • 静态绑定:静态绑定又称为前期绑定,表示在程序编译期间就可以确定程序完整的行为,函数的普通调用就是静态绑定,在编译时,就能明确哪个函数会被调用,动态绑定也可称为编译时决议。
  • 动态绑定:在多态调用的场景下,需要在程序运行时,根据虚函数表中的函数地址,来确定调用哪个函数,即:程序运行起来之后才能明确程序的具体行为,动态绑定也可称为运行时决议。
  • 函数普通调用为编译时决议,函数的多态调用为运行时决议。

二. 多继承中的虚函数表

2.1 虚函数表的打印

为了探索多继承中虚函数的行为,我们需要定义一个PrintVFTalbe函数,来打印虚函数所存储的地址,并通过虚函数表中存储的函数指针调用对应的函数,来观察虚函数表中的函数指针与子类和父类虚函数的指向关系。

因为VS编译器会在虚函数表末尾位置存储nullptr,所以使用Table[i] != nullptr作为循环结束的判断条件(Linux gcc编译器不会将在虚函数表最后放nullptr,必须显示地给定虚函数表中存储的函数指针的个数)。在每层循环内部,先打印函数指针(函数首条指令地址),然后将函数指针变量赋值给ptr,通过函数指针调用函数。

演示代码2.1:(虚函数表打印函数)

typedef void (*VFPTR)();  //将指向无参数、返回void的函数的函数指针类型重定义为VFPTR

void PrintfVFTable(VFPTR* table)
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("第%d个虚函数的地址:%p -> ", i, table[i]);
		VFPTR ptr = table[i];   //获取函数指针
		ptr();   //通过函数指针调用函数
	}
	std::cout << std::endl;
}

2.2 多继承中虚函数表中的内容存储情况

编写演示代码2.2,其中定义了两个父类Base1和Base2,两个父类中都定义了func1和func2虚函数,并且,在子类Derive中,重写func1函数,并且定义了一个新的虚函数func3。

调试代码,打开监视窗口,我们可以发现,子类对象中包含的亮哥父类对象各有一张虚函数表,但是,VS的监视窗口并没有显示出虚函数func3的地址,这并不是说func3的地址没有进虚函数表,而是VS编译器没有将其显示出来,可以认为这是编译器的一个小BUG。

演示代码2.2:

class Base1 
{
public:
	virtual void func1() { std::cout << "Base1::func1" << std::endl; }
	virtual void func2() { std::cout << "Base1::func2" << std::endl; }
private:
	int _b1 = 1;
};

class Base2 
{
public:
	virtual void func1() { std::cout << "Base2::func1" << std::endl; }
	virtual void func2() { std::cout << "Base2::func2" << std::endl; }
private:
	int _b2 = 2;
};

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

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;

	PrintfVFTable((VFPTR*)*(int*)ptr1);  //打印Base1的虚函数表
	PrintfVFTable((VFPTR*)*(int*)ptr2);  //打印Base2的虚函数表

	return 0;
}
图2.1 VS2019调试演示代码2.2的监视窗口

由于VS编译器的这个小“bug”,就要求我们显示的打印虚函数表,将虚函数指针作为参数,传给虚函数表打印函数。可见。Base1的虚函数表中存储了三个韩式指针,从前到后依次为:子类定义的func1、Base1中定义的func2、func3,Base2的虚表中存储了3个函数指针,从前到后依次为:子类中定义的func1、Base2的func2。

图2.2  多继承体系中的虚表及虚表中存储的内容

根据图2.2所示的虚表打印情况,总结出多继承体系中如下的规律:

  1. 在多继承体系中,每一个基类都有一张虚表。
  2. 如果两个基类之中存在同名的虚函数,同时在派生类中对同名的虚函数重写,那么这两个派生类中的虚函数都会被覆盖。
  3. 派生类中未被重写的虚函数,会被存入第一个基类的虚表之中。
图2.3 多继承体系中的内存模型

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

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

相关文章

Docker部署SpringBoot+Vue项目

1.项目部署规划 1.后端多模块项目blog以及各模块运行端口&#xff1a; 前台服务模块sangeng-blog->7777&#xff0c;后台服务模块sangeng-admin->8989&#xff0c;公共模块sangeng-framework 2.前端前台Vue项目&#xff1a;sg-blog-vue->80 3.前端后台Vue项目&#x…

如何在AWS EKS上部署安装nginx ingress controller

Ingress Controller Ingress Controller 通常是一个负载均衡器&#xff0c;用于将外部流量路由到您的 Kubernetes 集群&#xff0c;并负责 L4-L7 网络服务 Ingress controller 仅覆盖 L7 流量&#xff0c;而入口重新路由 HTTP 和 HTTPS 流量 Type of Ingress Controllers C…

QT编程集成环境在Ubuntu中如何使用ROS工程?

文章目录 0.引言1.安装Qt Creator&#xff08;带ROS插件&#xff09;2.创建ROS工程3.创建功能包4.创建节点5.添加编译规则6.编译运行 0.引言 在进行ROS开发过程中&#xff0c;会创建许多功能包和源代码文件&#xff0c;这些文件少量时&#xff0c;手动管理还能接受&#xff0c;…

微搭低代码学习之数据收集

低代码和开发之间的关系 低代码平台是一种快速构建应用程序的工具&#xff0c;旨在提高开发效率。它们提供了一种基于图形用户界面的方式来创建应用程序&#xff0c;而无需编写大量的代码。使用低代码平台&#xff0c;开发人员可以更快速地构建和交付应用程序&#xff0c;从而缩…

nginx--HTTPS服务

目录 1.为什么要使用https 2.https协议的实现 1.对称加密 2.非对称加密 3.https加密 3.生成密钥和CA证书需要的依赖 1.查看是否有装openssl 2.查看nginx是否有 --with-http_ssl_module 4.生成密钥和CA证书步骤 步骤一、生成key密钥 步骤二、通过密钥去生成证书签名请求文件…

Java多例Bean的应用场景-easyExcel导入

目录 1. bean注入方式&#xff08;IOC&#xff09;2. 有状态会话bean和无状态会话bean3. 单例模式和多例模式4. 深挖多例模式应用场景&#xff1f; 1. bean注入方式&#xff08;IOC&#xff09; 2. 有状态会话bean和无状态会话bean 有状态会话bean&#xff1a;每个用户有自己特…

2023什么电脑配置适合机器学习和人工智能

机器学习和人工智能应用有多种类型——从传统的回归模型、非神经网络分类器和以 Python SciKitLearn 和 R 语言的功能为代表的统计模型&#xff0c;到使用 PyTorch 和 TensorFlow 等框架的深度学习模型. 在这些不同类型的 ML/AI 模型中&#xff0c;也可能存在显着差异。“最佳”…

不用ChatGPT,只用CodeGeeX with Chat!一样实现智能问答

在ChatGPT推出后&#xff0c;许多人发现&#xff0c;它在编程方面也具有强大的能力——在编写代码过程中&#xff0c;如果遇到问题&#xff0c;可以不必去搜索引擎寻找答案&#xff0c;而是直接向ChatGPT提问。不过&#xff0c;在申请使用一些功能时&#xff0c;需要先等待各种…

机器人感知与控制关键技术及其智能制造应用

源自&#xff1a;自动化学报 作者&#xff1a;王耀南 江一鸣 姜娇 张辉 谭浩然 彭伟星 吴昊天 曾凯 摘 要 智能机器人在服务国家重大需求, 引领国民经济发展和保障国防安全中起到重要作用, 被誉为“制造业皇冠顶端的明珠”. 随着新一轮工业革命的到来, 世界主要工业国…

【Linux】System V IPC-进程信号

进程信号 信号的概念信号的产生信号的种类信号的处理方式信号的注册信号的注销信号的自定义处理方式信号的捕捉流程信号的阻塞常见的程序崩溃父子进程进程等待自定义信号处理方式volatile关键字 信号的概念 信号是一个软件中断&#xff0c;实际上是操作系统告诉进程需要进程执行…

08 - 文章详情页面

文章详情页面 8-1&#xff1a;开篇 从本章开始我们要进入文章详情的页面开发。 在文章详情页面可以展示&#xff1a; 文章标题作者信息发布时间文章内容文章评论 同时你可以在这里进行&#xff1a; 作者关注文章评论文章点赞文章收藏 等操作。 基本功能大家可以进入到我…

IT知识百科:什么是暴力破解?

暴力破解是一种常见的网络安全攻击方法&#xff0c;它利用计算机程序自动尝试大量的密码组合来破解密码。这种攻击方法通常用于获取未经授权的访问权限&#xff0c;如入侵网络系统或个人账户。在本文中&#xff0c;我们将探讨暴力破解的原理、工具和防范方法。 暴力破解的原理 …

WPS表格的重复项使用方法

重复项就是指一列内容中有重复一样的值&#xff0c;或者两列数据对比后是否有重复的值&#xff0c;而在WPS表格中提供了直接标记重复值&#xff08;高度重复项&#xff09;&#xff0c;删除重复值和限制重复值在一个单元格区域中输入。 【WPS表格的高度重复项】 作用是&#…

【K8S系列】深入解析DNS

序言 世界上最幸福的事之一&#xff0c;莫过于经过一番努力后&#xff0c;所有东西正慢慢变成你想要的样子。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记一级论点蓝色&#xff1a;用来标记二级论点 Kubernet…

数据挖掘——KNN算法的实现

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;练习时长两年半的java博主 &#x1f4d6;个人主页&#xff1a;君临๑ &#x1f381; ps&#xff1a;点赞是免费的&#xff0c;却可以让写博客的作者开心好几天&#x1f60e; 文章目录 一、k-最近邻分类算法介绍 二、k-NN的特…

C++ 简介

C 完全支持面向对象的程序设计&#xff0c;包括面向对象开发的四大特性&#xff1a; 封装&#xff08;Encapsulation&#xff09;&#xff1a;封装是将数据和方法组合在一起&#xff0c;对外部隐藏实现细节&#xff0c;只公开对外提供的接口。这样可以提高安全性、可靠性和灵活…

soot中存在的主要对象、soot的执行流

soot代码分析框架的基础知识&#xff08;二&#xff09;_soot 代码分析_小作坊中搬砖的博客-CSDN博客 Soot中的结构 本篇内容简单概括一下&#xff1a;soot中存在的主要对象、soot的执行流。 Soot中提供了几种对象&#xff0c;分别是&#xff1a;Scene、SootClass、SootMetho…

如何在 Windows WSL 上安装 k3s (Kubernetes / k8s)

WSL&#xff0c;全称Windows Subsystem for Linux&#xff0c;是微软在Windows 10操作系统上开发的一种运行Linux应用程序的子系统。它允许用户在Windows系统中直接运行Linux命令行工具和应用程序&#xff0c;无需双重引导或虚拟机。 相比虚机&#xff0c;WSL提供了更加高效、…

能量密度的必要性:城市比乡村具有更高的能量密度

文章目录 引言I 人口密度1.1 人口密度太低对于经济的发展的不利因素1.2 足够的人口密度带来的好处1.3 乌鲁克城II 农耕文明和商业文明2.1 农耕文明2.2 商业文明III 有效掌握动力的文明处于优势3.1 苏美尔人- 轮子&风能的利用3.2 英国人- 以蒸汽机为代表的工业革命引言 文明…

LeetCode——根据二叉树创建字符串与二叉树的最近公共祖先

606. 根据二叉树创建字符串 给你二叉树的根节点 root &#xff0c;请你采用前序遍历的方式&#xff0c;将二叉树转化为一个由括号和整数组成的字符串&#xff0c;返回构造出的字符串。 空节点使用一对空括号对 “()” 表示&#xff0c;转化后需要省略所有不影响字符串与原始二…