关于vs下多态虚表中存储的地址和实际成员函数地址不一样的原因

news2024/11/13 8:39:38
以如下代码为例:
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 (*func)();
void print(func a[])
{
        for (size_t i = 0; a[i] != nullptr; ++i)
        {
               printf("[%d]:%p\n", i, a[i]);
               a[i]();
        }
}
int main()
{
        Derive d;
        func* v1 = (func*)(*(int*)&d);
        PrintVTable(v1);
        func* v2 = (func*)*(int*)((char*)&d + sizeof(Base1));
         //&d得到的是Derive*类型的,+1会跳过sizeof(Derive)个字节,所以&d要强转为char*类型,
        //然后取对应位置的前四个字节,就拿到第二个虚表的地址了
        PrintVTable(v2);
        return 0;
}
代码打印了两张虚表,两张虚表中func1都是Derive类中func1的重写,但是他们的地址却不一样。
那么为什么都调用了Derive::func1显示的地址却不一样?
先说答案:因为这两个地址都不是真正的函数地址,真正地址的取用方式:&Derive::func1(cout是ostream类型的对象,但cout没有对函数指针类型进行适配,用cout打印函数指针会出问题)。虽然地址不一样,但是从调用结果来看,调用的都是Derive::func1函数。这和vs的实现有关,在Linux下看,地址可能就一样了。
图示可以发现Derive::func1的实际地址和虚表存的两个地址都不一样。
这是因为vs对成员函数实现了封装,虚表中储存的地址是封装位置的地址,虽然存储的地址不一样,但是最终还是调用到了Derive::func1,说明还是跳转到func1的位置了。
下面需要理解一些汇编语言的能力,这是程序员的基本素养,汇编语言不需要全看懂,大致看懂,了解在做什么就足够了。遇到不理解的,上网查查就足够了。
a[0]是第一个虚函数的地址,即虚表中存储的Derive::func1的地址
转到反汇编(需要在调试状态下才能转)
其中call是函数跳转,_RTC_CheckEsp是检查越界行为的函数,汇编代码中只有对edx的跳转不清楚情况,说明对edx的跳转可以到达真实Derive::func1的地址,那么看看edx中存储了什么(汇编代码也是可以调试的,逐语句,逐过程都可以,方括号代表其中存储的是地址,可以进行解引用):
结果发现edx中存储的就是虚表中的函数地址(在监视界面看edx的值时需要转为16进制显示)。
再对edx进行跳转,来到004A123F处。这里的jmp指令和call指令类似,可以继续跳转。
到这里就可以看出汇编指令在进行压栈(push),说明到这里就建立函数栈帧了,即已经到达真实的Derive::func1函数地址处。但是又和打印的Derive::func1地址不同。
刚才看的是base1中虚表地址的跳转,现在来看看base2的:
和之前的方式一样:
一路跳转,最终依然能看到跳转到建立栈帧的场景。而且和第一个虚表中最终跳转的位置一样。虚表中存储的地址是edx存储的地址。所以成员函数的真实地址和虚表中存储的地址不一致的原因是:虚表中存储的是实际地址的"跳板",通过不同路径最终跳转到真实地址的所在之处。
信息集中处理一下:
为了方便理解,下面的V1table为指向Base1虚表的数据,V2table为指向Base2虚表的数据。
仔细观察第二个虚表比第一个虚表多出来的地方。发现多了一个sub  ecx,8的步骤,sub就是减,也就是说要ecx减8,这里的8是Base1的大小。虽然没有传,但编译器认为调用第一个虚表是Base1指针(即V1table的地址)在调用,调用第二个虚表是用Base2指针(即V2table的地址)调用,所以在通过Base2指针调用虚函数时,会有一个减去Base1大小的步骤,因为fun1是被Derive重写的函数,需要对齐到Derive位置来找对应的虚函数。实际上,我们是通过从虚表中拿指针的方式调用成员函数,所以这里的减8,并没有什么用。但是从中也能看到Base2类型指针调用Derive::func1的机制。
演示代码中,两个虚表指针相差了20。这是因为虚表是一个数组,这里差的是两张虚表之间首元素地址的位置。相差为8的是V1table的地址和V2table的地址,即图中Base1->vTable的地址和Base2->vTable的地址相差8。
下面用
Base1* ptr1 = &d;
Base2* ptr2 = &d;
ptr1->func1();
ptr2->func1();
验证一下猜想。
首先,可以看到确实调用了同一个函数。然后转到反汇编:
可以看到汇编代码和猜想的一致,也说明了虚表中存储的函数指针不同的原因。V2table的地址相对于Derive位置有一个偏移量(在用Derive地址对Base2指针进行赋值时,会将Derive中Base2位置的地址切片赋值给Base2指针),通过Base2指针调用成员函数,需要修正偏移量(V1table地址和Derive的位置相同,不需要修正),到Derive的位置去找相应的函数。这就导致了Base1和Base2的指针需要通过不同路径找到Derive::func1,所以虚表中存储的地址也就不同。
其实理解这里的知识点并没有什么意义,主要是了解可以通过这样的方法深入了解原理,知道可以通过什么方法来寻找答案。
在继承章节中虚拟继承的虚基表也与这个有关。
class A
{
public:
        virtual void func()
        {}
public:
        int _a;
};
class B : public A
{
public:
        virtual void func()
        {}
public:
        int _b;
};
class C : public A
{
public:
        virtual void func()
        {}
public:
        int _c;
};
class D : public B, public C
{
public:
        int _d;
};
int main()
{
        D d;
        d.B::_a = 1;
        d.C::_a = 2;
        d._b = 3;
        d._c = 4;
        d._d = 5;
        return 0;
}
如图所示代码,此时A中有虚函数,B,C继承A,并对A中的虚函数进行重写。编译正常,B,C中各有一个虚表,可以储蓄对A中虚函数的重写,但是,如果B,C是虚拟继承就会报错,在虚拟继承下,B,C中存储A的未知统合为一块区域,(B,C中存储指向虚基表的指针,指针指向的虚基表中存储有对A区域的偏移量)这样就会导致编译器不知道在A的虚表中储存B中的重写,还是C中的重写。如果非要虚拟继承,就必须在继承了B,C的D中重写A中的虚函数。(虚基表和虚表没有关系)如图:
B,C都虚继承A,且对A中的虚函数重写会报错
D中存储的数据模型
B,C虚基表中存储有对A的偏移量
当B中增加一个A中没有的虚函数,B就会存在虚表,虚基表就会发生变化:
func1是B中的虚函数,所以不能放在A的虚表中,B就需要创建虚表。计算机中存储的是补码,所以fffffffc代表-4(ffffffff是-1),代表当前位置(虚基表指针的位置)对虚表的偏移量。所以虚表位置就在虚基表位置-4距离上。

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

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

相关文章

BES 平台 SDK之LED的配置

本文章是基于BES2700 芯片&#xff0c;其他BESxxx 芯片可做参考&#xff0c;如有不当之处&#xff0c;欢迎评论区留言指出。仅供参考学习用&#xff01; BES 平台 SDK之代码架构讲解二_谢文浩的博客-CSDN博客 关于SDK 系统框架简介可参考上一篇文章。链接如上所示&#xff01…

学python需要下载什么软件,自学python需要安装什么

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;学python需要安装一些什么软件好&#xff0c;学python需要安装一些什么软件&#xff0c;今天让我们一起来看看吧&#xff01; 工欲善其事必先利其器。初学者在学Python的时候&#xff0c;往往会因为没有好用的软件工具&…

2.5 BUMP图改进

一、Bump Mapping介绍 凹凸贴图映射技术是对物体表面贴图进行变化然后进行光计算的一种技术。例如给法线分量添加噪音&#xff0c;或者在一个保护扰动值的纹理图中进行查找。这是一个提升物理真实感的有效方法&#xff0c;但却不需要额外的提升物体的几何复杂度。这种法式在提…

华为OD机试真题 Java 实现【简单的自动曝光】【2023Q1 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、备注五、解题思路六、Java算法源码七、效果展示1、输入2、输出3、说明4、再输入5、输出6、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff…

【雕爷学编程】Arduino动手做(173)---SG90舵机双轴云台模块2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

数据库事务--springboot事务处理

二、简单事务使用 模拟一 1、数据库 db.sql drop database if exists supermarket; create database supermarket; use supermarket; drop table if exists order_item; CREATE TABLE order_item (item_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 订单ID,order_no …

Linux 用户和权限

一、root 用户 root 用户(超级管理员) 无论是windows、Macos、Linux均采用多用户的管理模式进行权限管理。在Linux系统中&#xff0c;拥有最大权限的账户名为&#xff1a;root (超级管理员)。 root用户拥有最大的系统操作权限&#xff0c;而普通用户在许多地方的权限是受限的。…

一文教你吃透Git工具

目录 Git简介 git的安装和配置 新建仓库 工作区域和文件状态 git reset回退版本 使用git diff比较差异 Git简介 git是一个免费开源的分布式版本控制系统&#xff0c;它使用一个特殊的叫做仓库的数据库来记录文件的变化&#xff0c;仓库中的每个文件都有一个完整的版本历史…

windows10 wifi情况下修改静态IP

1.鼠标右击网络选择属性 2.选择更改适配器设置 3.在WLAN&#xff0c;右击选择属性 4. 双击Internet 协议版本4(TCP/IPv4) 5. 启用“使用下面的IP地址” 6.输入目标IP地址&#xff0c;子网掩码&#xff0c;默认网关 我以192.167.1.2为例&#xff1a; 7.cmd中ipconfig查询验证…

【驱动开发day8作业】

作业1&#xff1a; 应用层代码 #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <sys/ioctl.h>int main(int…

P3855 [TJOI2008] Binary Land

[TJOI2008] Binary Land 题目背景 Binary Land是一款任天堂红白机上的经典游戏&#xff0c;讲述的是两只相爱的企鹅Gurin和Malon的故事。两只企鹅在一个封闭的迷宫中&#xff0c;你可以控制他们向上下左右四个方向移动。但是他们的移动有一个奇怪的规则&#xff0c;即如果你按…

【技术讨论】RF环境搭建手册

简要整理下环境搭建的步骤&#xff0c;以便快速、准确的搭建测试环境。 一、环境搭建 一、Python 2.7 1、 不要用Python3.6&#xff0c;很多库3.6中还没有&#xff0c;wxPython官方只支持Python 2。 2、 环境变量配置后需要重启才能生效。 3、 环境变量添加C:\Python27\Sc…

k8s概念-deployment

deployment用于部署无状态应用 Deployment集成了上线部署、滚动升级、创建副本、回滚等功能 Deployment里包含并使用了ReplicaSet Replicaset 通过改变Pod副本数量实现Pod的扩容和缩容 参考文档 https://kubernetes.io/zh-cn/docs/concepts/workloads/controllers/deployment/ …

go Channel

channel 单纯地将函数并发执行是没有意义的。函数与函数之间需要交换数据才能体现出并发执行函数的意义。 虽然可以使用共享内存进行数据交换&#xff0c;但是共享内存在不同的goroutine中很容易发生竞态问题。为了保证数据交换的准确性&#xff0c;必须使用互斥量对内存进行…

ptyhon——案例五:设定:list=[0,1,2,3,4,5] 列表,翻转列表

案例五&#xff1a;设定&#xff1a;list[0,1,2,3,4,5] 列表&#xff0c;翻转列表def Reverse(lst):return [ele for ele in reversed(lst)] #翻转列表 lst[0,1,2,3,4,5] print(Reverse(lst))

insert into select用法

文章目录 一、insert into select二、insert into select插入失败 本篇文章主要讲解insert into select 的用法&#xff0c;以及insert into select的坑或者注意事项。本篇文章中的sql基于mysql8.0进行讲解 一、insert into select 该语法常用于从另一张表查询数据插入到某表中…

hcip——BGP实验

要求 1.搭建toop 2.地址规划 路由器AS接口地址R11 loop0:1.1.1.1 24 loop1 : 192.168.1.1 24 g0/0/0 12.0.0.1 24 R22 64512 g0/0/0: 12.0.0.2 24 g/0/01: 172.16.0.2 19 g0/0/2: 172.16.96.2 19 R32 64512g0/0/0: 172.16.0.3 19 g0/0/1:1…

【LeetCode】88. 合并两个有序数组

这道题我总共想了三种解法。 1.将nums2中的元素依次放入nums1有效元素的后面&#xff0c;再总体进行排序。 import java.util.*; class Solution {public void merge(int[] nums1, int m, int[] nums2, int n) {int j 0;for(int i m;i<mn;i){nums1[i] nums2[j];j;}Arrays…

【PostgreSQL】系列之 一 CentOS 7安装PGSQL15版本(一)

目录 一、何为PostgreSQL&#xff1f; 二、PostgreSQL安装 2.1安装依赖 2.2 执行安装 2.3 数据库初始化 2.4 配置环境变量 2.5 创建数据库 2.6 配置远程 2.7 测试远程 三、常用命令 四、用户创建和数据库权限 一、何为PostgreSQL&#xff1f; PostgreSQL是以加州大学…

2023.08.01 驱动开发day8

驱动层 #include <linux/init.h> #include <linux/module.h> #include <linux/of.h> #include <linux/of_irq.h> #include <linux/interrupt.h> #include <linux/fs.h> #include <linux/gpio.h> #include <linux/of_gpio.h>#…