【汇编 C++】多态底层---虚表、__vfptr指针

news2025/7/18 5:59:14

前言:如果对多态不太了解的话,可以看我的这篇文章《C++多态》,另外本文中出现到的汇编代码,我都会予以解释,看不懂没关系,知道大概意思就行,能不讲汇编的地方我就不讲;

           本文使用到的工具是vs2010;

目录

虚表是什么? 

        实现多态的关键-virtual关键字底层

        虚表占用字节数

        总结

虚表的结构

虚表的内容

        虚表中函数的调用

总结

结语


虚表是什么? 

        虚表也被称为虚函数表,我们知道,实现多态的主要因素还是virtual这个关键字,加上这个关键字的函数被称为虚函数,C++中的虚函数的实现一般是通过虚函数表(C++规范并没有规定具体用哪种方法,但大部分的编译器厂商都选择此方法);

        为了引出所谓的虚表,我们要先从virtual关键字下手;

        实现多态的关键-virtual关键字底层

        我们知道实现多态的关键是virtual关键字,那么带不带virtual关键字在底层有什么区别呢?测试代码test01,如下:

#include <stdio.h>
#include <Windows.h>>

class Person
{
public:
	int x;
	void print()
	{
		printf("父类输出。\n");
	}
};

class Teacher:public Person
{
public:
	void print()
	{
		printf("子类输出。\n");
	}
};

void Func(Person* p)
{
	p->print();                        // 此处下断点
}

int main()
{
	Person p;
	Teacher t;

    Func(&t);           // 因为父类指针可以指向子类对象,所以这里没问题

	return 0;
}

        首先,创建一个父类,父类中有成员x和函数print;子类Teacher继承Person;重写print函数,然后在主函数中,我们定义一个父类对象一个子类对象就行了;

        我们知道不使用virtual关键字的时候,是不能实现多态的,所以这里我们在p->print()这个可能实现多态的位置下一个断点,观察其反汇编:

        f7编译,f5运行,alt+8转到反汇编:

        我们给父类函数加上virtual关键字:

        再次查看汇编代码:

        我们大致可以总结一下带virtual关键字和不带virtual关键字的时候有什么区别?也就是说virtual是如何实现多态的:

        不带virtual的时候,底层是直接调用,已经指定了Person的print函数,无法实现多态;

        带virtual的时候,底层是间接调用,只是给出了一个地址,可以是Person的print也可以是Teacher的,可以实现多态;

        知道了virtual关键字实现多态的关键了,下面我们来分析一段virtual底层的汇编代码:

        就分析这一段吧;

        首先我们来看第一段汇编指令:mov eax,dword ptr [p];

        他的意思就是将p这个地址中的内容存放到eax寄存器里;

        那么p是什么呢?p就是我们传给print()函数的参数,这里我们传入的是子类的对象t,那么p就是 Person* p = &t;是一个指向子类对象首地址的指针,那么上面这一行汇编的意思就是将子类对象t的首地址存放到eax中,那么此时的eax中存放的是&t;

        然后看第二段汇编:mov edx,dword ptr [eax];

        没错,他的意思是将eax中&t这个地址中的值取出存放到edx中;

        那么&t这个地址中的值是什么呢?因为&t是对象t的首地址,所以&t中的值就是t的第一个成员啊,看到这里你可能会觉得t的第一个成员不就是继承过来的x嘛,其实不是的,我们继续看;

        已知我们现在将t的第一个成员存放到edx中了;

        然后看第三段汇编:mov esi,esp;

        这个汇编没必要看,只是备份一下esp的值,等会用来比较堆栈是否平衡;

        然后看第四段汇编:mov ecx,dword ptr[p];

        这段汇编应该能看懂了吧,将p中对象t的首地址放在ecx中;

        然后看第五段汇编:mov eax,dword ptr [edx];

        重点来了重点来了重点来了!!!

        让我们回忆一下,此时edx中存放的是t的第一个成员,我们取第一个成员地址中的值放到eax中,如果第一个成员是x的话,我们取x这个地址中的值,这是违法的不被允许的;为什么?x它是一个int类型的变量,他不是一个地址,x的值是我们赋给他的值,如果我们给x赋值1,那么&0x1这个地址中的值,操作系统是不允许的,因为我们没有权限;

        所以t的第一个成员肯定不是变量x!!!记住这个结论。

        我们明明只定义了一个x啊,如果不是x还能是什么?难道说......另有其人?那它是什么呢?

        下面为大家揭秘:

        虚表占用字节数

        想要知道t的第一个成员是什么,我们首先要知道t他到底有几个成员;我们用最简单的测试:sizeof()函数;

        首先我们还是先去掉virtual关键字

        然后sizeof一下Person和Teacher的对象看一下大小:

        都是四个字节,也就是成员x;

        那么我们加上virtual关键字看一下大小:

         竟然变成了8个字节!!!!

        我们现在大概可以确定,加上virtual关键之后,类中另有成员!!!不止变量x; 

        不急,我们先看一下,如果我们定义两个成员的话,大小是多少呢?

        运行输出:

        全变成12字节了;

        我们现在大致可以确定虚函数virtual在类中有一个大小为4字节的虚表 

        那么是不是说,一个virtual虚函数的虚表就占用4个字节?两个占用两个?

        测试一下,再加一个虚函数

        运行输出:

        还是12;

        其实从定义我们就可以知道,虚表之所以叫表,肯定不止是存放一个地址啊,肯定可以存放很多,但是为什么一整张表能够做到只占用四个字节?因为我们传入的当然不可能是一整张表,而是表的首地址,是一个名叫__vfptr的指针,我们下面会说;

        到现在我们大致可以总结一下了;

        总结

        我们可以确认了,如果类中具有virtual修饰的虚函数,无论有多少个虚函数,类的对象在底层中只有一个虚表,因为我们传入的是虚表的首地址,只占用四个字节,并且这个虚表首地址是类对象的第一个成员!

虚表的结构

        知道虚表的位置了之后,我们就要研究虚表的结构了,究竟是一个什么样的四字节?可以存放那么多函数的地址;

        已知此时我们的父类拥有两个虚函数,两个对象,子类重写了两个虚函数;

        我们在main函数中就定义两个对象,然后调用Func函数,然后在return 0;的地方下断点:

        考虑到大家可能不了解汇编,所以这里我按照编译器给出的结果去给大家讲,能不用汇编就不用汇编了;

        我们f5调试一下:

        随便找一个监视窗口:

        然后我们在窗口中输入对象p和对象t,代表监视这两个对象:

        我们点开对象旁边的"+",可以看到对象中依次排列的成员:

 

        我们可以看到,p的第一个成员是一个叫做__vfptr的东西,他是一个指针,指向我们虚函数表的首地址,他指向的地址里面存放的就是我们所说的虚函数表;先看父类,__vfptr其实就是一个地址,占四个字节;

        继续往下,我们点开__vfptr旁边的+号:

        里边存放了两个函数?我们现在大胆猜一下,__vfptr这个指针(虚表的首地址)指向的地址存放了这两个虚函数的调用地址,是这样吗?我们把0x00c55740复制到内存窗口,看一下:

        是的,没错;__vfptr指针指向的地址中存放了其他虚函数的地址;

        我们大致可以总结一下虚表的结构了。

        为什么只占用四个字节的虚表,可以存放很多虚函数的地址?

        因为我们类的底层存放的并不是一整张表,而只是虚表的首地址,也就是一个名叫__vfptr的指针,这个指针指向的地址中存放了其他所有虚函数的调用地址;

虚表的内容

        我们现在只是模糊的知道了,虚表的内容是其他函数的调用地址,但是都是哪些函数呢?我们暂且还不知道;因为我们还没有测试,下面我们来进行测试;

        其他的代码不变,主函数中,传入&p;

         为了方便看p对象与t对象虚函数的成员,我们断点打在return 0;这里:

        点开旁边的"+"号:

 

        我们发现,对象p的虚函数表中存放着两个函数,一个Person::print(),一个是Person::print1(),对象t也一样,存放着两个函数,一个是Teacher::print(),一个是Teacher::print1();

        那么,我们知道,这是我们子类重写的情况下,才是这样一个结果,父类的表中存放父类成员,子类的表中存放子类成员;

        但是,我们也知道,只要父类写了virtual虚函数,不管子类有没有重写,底层对函数的调用都是间接调用,不是直接调用;

        也就是说,无论子类有没有重写,在Func函数中p->print()这里的调用,子函数都必须传入一个__vfptr虚表,那么如果子类没有重写父类的虚函数时,虚表中存放的是什么呢?

        我们继续测试:

        现在我们将子类的重写全部删掉,如下:

        其他代码不变,断点依旧return 0;调试一下:

        我们看到,父类中还是存放了父类的虚函数成员,这个时候子类因为没有重写,也存放了父类的虚函数成员;

        我们现在可以大致总结一下虚表的内容了;

        如果父类有n个虚函数那么父类的虚表中就存放这n个虚函数的地址;子类的虚表中也是存放了n个函数的地址,如果子类中重写了父类的虚函数,那么子类虚表中就用子类自己重写后的函数地址,如果子类中有的函数没有重写父类的虚函数,那么虚表中的该调用地址就用父类的函数;

        虚表中的函数如何调用

        已知,我们现在已经很清楚的了解了虚表中的内容,就是存放了待调用函数的地址;那么调用函数的时候底层是怎么调用的呢?

        我们先调用p的print函数,print函数是p虚表的第一个成员,断点打在函数调用的地方:

        f5调试,alt+8转到反汇编:

 

        f11跟进,到call eax的内部,看它调用的是谁:

 

        是Person::print();没错,调用print()时,我们传入的是edx(虚表的首地址);

        我们把调用的函数换成print1():

 

        其他不变,继续调试,转到反汇编:

 

        我们可以看到,这次调用的地址是edx+4;我们已知edx就是虚表的首地址,那么edx+4中存放是什么呢?

        f11跟进:

 

        可以看到edx+4存放的是print1();也就是第二个虚函数的调用地址;

        那么我们现在可以总结一下,虚表中的那么多的函数地址是怎么调用的呢?

        通过指针偏移调用!

        从编译结束的那一刻起__vfptr只是一个首地址,是一个指针,__vfptr+4就是第二个函数的地址,可想而知+8就是第三个虚函数的地址,当然前提是你有第三个虚函数;

总结

        1、虚表就是一个存放了很多函数调用地址的表,我们类的第一个成员是虚表的首地址,只占用四个字节;

        2、如果父类有n个虚函数那么父类的虚表中就存放这n个虚函数的地址;子类的虚表中也是存放了n个函数的地址,如果子类中重写了父类的虚函数,那么子类虚表中就用子类自己重写后的函数地址,如果子类中有的函数没有重写父类的虚函数,那么虚表中的该函数的调用地址就用父类的函数;

        3、虚函数的调用是通过指针偏移实现的,__vfptr本身就是一个指针,__vfptr+0中存放着第一个虚函数的调用地址,__vfptr+4中存放着第二个虚函数的调用地址,以此类推;

        4、含有虚函数的类的第一个成员是__vfptr,vfptr本身是一个指针,它指向的地址中存放了连续的四字节地址,也就是其他虚函数的调用地址,形成了一张表,称为虚表;我们类的底层的第一个成员是__vfptr指针,不是一整张表,这也就是为什么四字节的地址可以存放很多虚函数的原因;

结语

        本篇文章,个人感觉可能讲的比较绕,如果有哪个地方没有看懂,可以向我提出来,我们加以修改,尽量通俗易懂;如果有什么意见、建议或者错误都请提出来,谢谢大家!

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

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

相关文章

networkx学习记录

networkx学习记录networkx学习记录1. 创建图表2. 节点3. 边4.检查图的元素5.从图中删除元素6.使用图构造函数7.访问边和邻居8.向图、节点和边添加属性9.有向图10. 绘制图形networkx学习记录 1. 创建图表 创建一个空图 import networkx as nx G nx.Graph()此时如果报以下错误…

HTML网页设计结课作业——11张精美网页 html+css+javascript+bootstarp

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 精彩专栏推荐&#x1f4…

学姐突然问我键盘怎么选?原来是为了这个...

前言&#xff1a; 上个星期学姐来问我该买啥键盘&#xff0c;说是自己用的笔记本的键盘实在是不太好用&#xff0c;很喜欢机械键盘的手感&#xff0c;但是常规的机械键盘有太大了而且声音十分大&#xff0c;对她们女生来说并不是很友好。于是我给她推荐了我现在正在用的这款键盘…

头歌-信息安全技术-Java生成验证码

头歌-信息安全技术-Java生成验证码一、第1关&#xff1a;使用Servlet生成验证码1、任务描述2、编程要求3、评测代码二、第2关&#xff1a;用户登录时校验验证码是否正确1、任务描述2、编程要求3、评测代码三、第3关&#xff1a;使用Kaptcha组件生成验证码1、任务描述2、编程要求…

2023年前端开发未来可期

☆ 对于很多质疑&#xff0c;很多不解&#xff0c;本文将从 △ 目前企业内前端开发职业的占比&#xff1b; △ 目前业内开发语言的受欢迎程度&#xff1b; △ 近期社区问答活跃度&#xff1b; 等维度来说明目前前端这个职业的所处位置。 ☆ 还有强硬的干货&#xff0c;通过深入…

跳槽前恶补面试题,成功上岸阿里,拿到33k的测开offer

不知不觉间&#xff0c;时间过得真快啊。作为一名程序员&#xff0c;应该都清楚每年的3、4月份和9、10月份都是跳槽的黄金季&#xff0c;各大企业在这段时间会大量招聘人才。在这段时间里&#xff0c;有人欢喜有人悲。想必各位在跳槽前都会做好充足的准备&#xff0c;同样做足了…

详细讲解网络协议:TCP和UDP什么区别?

该文章是学习了 B 站 up 主的视频做的总结&#xff0c;讲的很通俗易懂&#xff0c;首先感谢博主的分享。视频地址&#xff1a;https://www.bilibili.com/video/BV1kV411j7hA/?spm_id_from333.337.search-card.all.click&vd_source0a3d4c746a63d737330e738fa043eaf6 重新认…

【HDU No. 3567】八数码 II Eight II

【HDU No. 3567】八数码 II Eight II 杭电OJ 题目地址 【题意】 八数码&#xff0c;也叫作“九宫格”&#xff0c;来自一个古老的游戏。在这个游戏中&#xff0c;你将得到一个33的棋盘和8个方块。方块的编号为1&#xff5e;8&#xff0c;其中一块方块丢失&#xff0c;称之为“…

【python】基础复习

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录python的应用基础语法编码标识符python保留字第一个注释多行语句数字(Number)类型字符串(String)print 默认输出是换行的&#xff0c;如果要实现不换行需要在变量末尾加上 end""&#xff1a;impor…

猿创征文|在校大学生学习UI设计必备工具及日常生活中使用的软件

嗨&#xff0c;大家好&#xff0c;我是异星球的小怪同志 一个想法有点乱七八糟的小怪 如果觉得对你有帮助&#xff0c;请支持一波。 希望未来可以一起学习交流。 我是一名在校大二的学生&#xff0c;目前在学习关于UI设计方向的一些课程&#xff0c;平时会用到UI设计必备的工…

我终于读懂了适配器模式。。。

文章目录&#x1f5fe;&#x1f306;什么是适配器模式&#xff1f;&#x1f3ef;类适配器模式&#x1f3f0;对象适配器模式⛺️接口适配器模式&#x1f3ed;适配器模式在SpringMVC 框架应用的源码剖析&#x1f5fc;适配器模式的注意事项和细节&#x1f306;什么是适配器模式&am…

基于SDN环境下的DDoS异常攻击的检测与缓解--实验

基于SDN环境下的DDoS异常攻击的检测与缓解--实验基于SDN环境下的DDoS异常攻击的检测与缓解--实验1.安装floodlight2.安装sFlow-RT流量监控设备3.命令行安装curl工具4.构建拓扑5.DDoS 攻击检测6.DDoS 攻击防御7.总结申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&am…

PNG怎么转换成PDF?这篇文章教会你

有时候我们需要查找一些图片资料并将它打印出来&#xff0c;但是在网上的图片大多是以PNG格式存在的&#xff0c;这个时候&#xff0c;我们就需要先利用一些转换软件把PNG转换成PDF文件的格式&#xff0c;从而方便我们进行打印。那么你们知道PNG转PDF怎么转换吗&#xff1f;今天…

第四章:前缀和、差分(数列)

前缀和差分一、前缀和1、 什么是前缀和2、 前缀和的作用3、 前缀和的例题和模板&#xff08;1&#xff09;一维数组的前缀和C版C版&#xff08;2&#xff09;二维数组的前缀和a.思路&#xff1a;b.题目和模板&#xff1a;C版C版二、差分1、什么是差分&#xff1f;2、差分有什么…

FFplay文档解读-43-视频过滤器十八

29.170 telecine 将电视电影处理应用于视频。 此过滤器接受以下选项&#xff1a; first_field选项解释top, ttop field firstbottom, b底部字段优先默认值为top pattern一串数字&#xff0c;表示希望应用的下拉模式。 默认值为23。 Some typical patterns:NTSC output (30i…

传统纸业如何实现数字化,S2B2C系统网站赋能渠道提升供应链管理效率

一千多年前&#xff0c;我们老祖宗发明了造纸术&#xff0c;纸张成为方便、廉价的信息载体&#xff0c;由此影响了中国乃至世界文明的进程。如今&#xff0c;随着信息技术的普及&#xff0c;纸张作为信息载体的功能日益弱化&#xff0c;但作为一种环保材料将会更广泛地融入我们…

通过宠物商店理解java面向对象

前言&#xff1a;本篇博客&#xff0c;适合刚刚学完java基础语法的但是&#xff0c;对于面向对象&#xff0c;理解不够深刻的读者&#xff0c;本文通过经典的宠物商店&#xff0c;来让读者深刻的理解&#xff0c;面向对象&#xff0c;IS-A&#xff0c;HAS-A法则。本文不仅仅是简…

Spring更简单保存和获取bean对象的方法(注解)

Spring更简单保存和获取bean对象的方法前置准备将bean对象更为简单地保存到Spring容器中&#xff08;使用注解&#xff09;【使用类注解】 (是写在类前的)为什么要这么多类注解&#xff1f;类注解之间的关系使用类注解 Bean 命名规则使用方法注解&#xff08;Bean&#xff09;不…

[MySQL]数据库的约束与表的设计

专栏简介 :MySql数据库从入门到进阶. 题目来源:leetcode,牛客,剑指offer. 创作目标:记录学习MySql学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 文章目录 1.数据库约束 1.1 约束类型 1.2 null 约束 1.…

Redis面试题

目录 面试题&#xff1a;谈谈你对Redis的理解&#xff1f; 面试题&#xff1a;Redis的基本数据类型 Redis的基本数据类型以及它们的应用场景&#xff1a; 面试题&#xff1a;redis内存淘汰机制 面试题&#xff1a;Redis持久化机制 RDB AOF 面试题&#xff1a;Redis写时复…