『 C++类与对象 』多态之单继承与多继承的虚函数表

文章目录

    • 🫧 前言
    • 🫧 查看虚表
    • 🫧 单继承下的虚函数表
    • 🫧 多继承下的虚函数表


🫧 前言

多态是一种基于继承关系的语法,既然涉及到继承,而继承的方式有多种:

  • 单继承
  • 多继承
  • 棱形继承
  • 棱形虚拟继承
    不同的继承方式其虚表的形式也不同;
以下操作均为在CentOS7_x64机器上的操作

🫧 查看虚表

已知虚表为一个void (*)()的函数指针数组,除了以内存的方式查看虚表以外还可以使用函数调用的方式来查看虚表的真实情况;
其思路即为将该指针数组的指针打印并调用;
根据函数调用可以知道哪个指针是哪个函数;

typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}
//函数的参数为函数指针数组(虚表)的首地址;
//由于是自定义类型的前4/8个字节(在该平台下为8个字节)
//应使用对应的方式取到前8个字节;
//通过该首地址向后进行遍历;

🫧 单继承下的虚函数表

存在一个单继承关系:

class A{//基类public:virtual void Func1(){//虚函数cout<<"A::Func1()"<<endl;}virtual void Func2(){//虚函数cout<<"A::Func2()"<<endl;}int _a = 10;
};class B:public A{//派生类public:virtual void Func1(){//虚函数且完成重写cout<<"B::Func1()"<<endl;}virtual void Func3(){//虚函数cout<<"B::Func3()"<<endl;}int _b = 20;
};void test1(){//分别实例化出两个对象A aa;B bb;
}

使用GDB打印出实例化出的aabb的内容;

(gdb) display aa
1: aa = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
(gdb) display bb
2: bb = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}

由于子类对象和父类对象种都存在一张虚表,所以对应的子类对象的虚函数存储于子类的虚表当中,父类对象的虚函数存储于父类的虚表当中;

其中该段所出现的结果中的_vptr.A = 0x400ad8_vptr.A = 0x400ab0即为虚表指针,该地址不是两个对象的地址,而是该对象地址中首地址所存储的内容;

可以使用&将两个对象的地址取出并使用x/x进行解析从而验证;

(gdb) p &aa 
$10 = (A *) 0x7fffffffe430	#aa对象的首地址
(gdb) x/x 0x7fffffffe430 
0x7fffffffe430:	0x00400ad8	#其首地址所存储的数据(gdb) p &bb
$11 = (B *) 0x7fffffffe420	#bb对象的首地址
(gdb) x/x 0x7fffffffe420
0x7fffffffe420:	0x00400ab0	#其首地址所存储的数据

其中上面的首地址所存储的数据即为一个指针,这个指针即为虚表(虚函数表)指针,也就是虚函数表的首地址位置;

在该示例中基类和派生类中各有两个虚函数,其中派生类的Func1()虚函数重写了基类的Func1()虚函数,所以在基类和派生类的虚表中都存在该函数,且该函数的地址不同;

  • A类虚表

    #	A类虚表
    (gdb)  p aa
    $12 = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
    #----------------------------------
    (gdb) x/x 0x400ad8	
    0x400ad8 <_ZTV1A+16>:	0x00400924	#虚表首地址所存储的数据(A::Func1()函数的地址)
    (gdb) x/x 0x00400924
    0x400924 <A::Func1()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ae0
    0x400ae0 <_ZTV1A+24>:	0x00400950	#虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址)
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    
  • B类虚表
    B类虚表与之不同的是,B类作为派生类,而派生类的虚表可以看成是基类虚表的拷贝,且若发生重写的话虚表中的那个被重写的函数将会被重写的函数进行覆盖;

    (gdb) p bb
    #	B类虚表
    $14 = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}
    #----------------------------------
    (gdb) x/x 0x400ab0
    0x400ab0 <_ZTV1B+16>:	0x0040097c	#虚表首地址所存储的数据(B::Func1()函数的地址[已被重写所以地址不同])
    (gdb) x/x 0x0040097c
    0x40097c <B::Func1()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ab8
    0x400ab8 <_ZTV1B+24>:	0x00400950	#虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址[派生类的虚函数表可以看成是基类函数表的拷贝])
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#将地址解析后得到函数
    #----------------------------------
    (gdb) x/x 0x400ac0
    0x400ac0 <_ZTV1B+32>:	0x004009a8	#虚表中第三个位置所存储的数据(由于是64位机器偏移量为8,B::Func3()函数的地址[这里存放的是B类中自身的函数])
    (gdb) x/x 0x004009a8
    0x4009a8 <B::Func3()>:	0xe5894855	#将地址解析后得到函数

在这里插入图片描述

使用函数查看:

typedef void(*VFPTR)();void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}void test1(){A aa;B bb;PrintVT(*(VFPTR**)&aa,2);PrintVT(*(VFPTR**)&bb,3);
}

结果为 (重新编译过所以导致最终结果不同,但结论相同):

ptr: 0x400c60第0地址:0x400a94,->A::Func1()第1地址:0x400ac0,->A::Func2()ptr: 0x400c38第0地址:0x400aec,->B::Func1()第1地址:0x400ac0,->A::Func2()第2地址:0x400b18,->B::Func3()

🫧 多继承下的虚函数表

多继承下的虚函数表较于单继承来说会更加的复杂;
复杂的原因在于多继承为多个基类继承给一个派生类,那么假设两个基类都有同名虚函数,且派生类重写了这个虚函数应该如何判断?

class A{public:virtual void Func1(){cout<<"A::Func1()"<<endl;}virtual void Func2(){cout<<"A::Func2()"<<endl;}
};class B{public:virtual void Func1(){cout<<"B::Func1()"<<endl;}virtual void Func2(){cout<<"B::Func2()"<<endl;}
};class C : public A,public B{public:virtual void Func1(){cout<<"C::Func1()"<<endl;}virtual void Func3(){cout<<"C::Func3()"<<endl;}
};void test2(){C cc;
}

存在以上的继承关系;

使用GDB调试该程序并打印cc的内容;

p cc
$9 = {<A> = {_vptr.A = 0x400cc0 <vtable for C+16>}, <B> = {_vptr.B = 0x400ce8 <vtable for C+56>}, <No data fields>}

由第一点可以知道,派生类的虚表可以看作是基类虚表的拷贝,那么在该程序中由于存在两个基类(多继承),所以应当也有两个虚表;

那么在这个继承关系中,派生类自身所增加的虚函数处于哪个虚表?

实际上在多继承关系中,派生类自身所增加的虚函数都在第一个虚表中,且第一张虚表不仅只存在派生类自身的虚函数,还有一个较为关键的数据;

  • 第一张虚表
    #-------64位机器偏移量为8---------
    #	C::Func1()	被重写
    (gdb) x/x 0x400cc0
    0x400cc0 <_ZTV1C+16>:	0x00400b56
    (gdb) x/x 0x00400b56
    0x400b56 <C::Func1()>:	0xe5894855
    #-------------------------------
    #	A::Func2() 
    (gdb) x/x 0x400cc8
    0x400cc8 <_ZTV1C+24>:	0x00400ad2
    (gdb) x/x 0x00400ad2
    0x400ad2 <A::Func2()>:	0xe5894855
    #-------------------------------
    #	C::Func3()	派生类自身
    (gdb) x/x 0x400cd0 
    0x400cd0 <_ZTV1C+32>:	0x00400b88
    (gdb) x/x 0x00400b88
    0x400b88 <C::Func3()>:	0xe5894855
    #-------------------------------
    (gdb) x/x 0x400cd8
    0x400cd8 <_ZTV1C+40>:	0xfffffff8 #关键数据
    #-------------------------------
    

从该结果可以观察到,派生类自身的虚函数位于第一张虚表当中;
且在最后一个位置存在一个0xfffffff8的数据;


  • 第二张虚表
    #-------------------------------
    #	所存数据并不为虚函数
    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81
    0x400b81 <_ZThn8_N1C5Func1Ev>:	0x08ef8348
    (gdb) x/x 0x08ef8348
    0x8ef8348:	Cannot access memory at address 0x8ef8348
    #-------------------------------
    #	B类中未重写的虚函数
    (gdb) x/x 0x400cf0
    0x400cf0 <_ZTV1C+64>:	0x00400b2a
    (gdb) x/x 0x00400b2a
    0x400b2a <B::Func2()>:	0xe5894855
    #-------------------------------
    #	NULL空
    (gdb) x/x 0x400cf8
    0x400cf8 <_ZTV1B>:	0x00000000
    #-------------------------------
    
    从该虚表中能看到第二张虚表的第一个位置所存储的数据并不是函数指针;
    在这里就可以提到对应的0xfffffff8数据;
    已知0xffffffff的值为-1,对应的0xfffffff8即为-8;
    这里的值其实是一个偏移量,这个偏移量:

    当走到该处时将该处的偏移量-8,即得到该处函数所在的位置;
    根据这个点进行验证;
    此时已经知道了位置为0x400ce8,且该位置所存储的数据为0x00400b81;

    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81-8
    0x400b79 <C::Func1()+35>:	0xfffcb2e8
    
    从这里就已经看出,这里通过了偏移量间接的找到了对应的函数;
    当编译器在处理这段代码时,将根据偏移量做出一些处理,使得最终能够通过该偏移量找到对应的函数;

结论为:若是出现多继承,其中两个基类都存在同名的虚函数且在派生类中对该虚函数已经完成了重写的条件时,其虚表构造为如下图:

在这里插入图片描述


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

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

相关文章

ToDesk提示通道限制 - 解决方案

问题 使用ToDesk进行远程控制时&#xff0c;免费个人账号最多支持1个设备同时发起远控&#xff0c;若使用此账号同时在2个设备发起远控&#xff0c;则会提示通道限制&#xff0c;如下图&#xff1a; 解决方案 方案1&#xff1a;断开其它远控 出现通道限制弹窗时&#xff0…

数据结构(超详细讲解!!)第二十四节 二叉树(下)

1.遍历二叉树 在二叉树的一些应用中&#xff0c;常常要求在树中查找具有某种特征的结点&#xff0c;或者对树中全部结点逐一进行某种处理。这就引入了遍历二叉树的问题&#xff0c;即如何按某条搜索路径访问树中的每一个结点&#xff0c;使得每一个结点仅且仅被访问一次。 …

python3实现tailf命令

由于windows上面没有类似linux上面的tailf命令&#xff0c;所以下面的python脚本来代替其能力。 tailf.py import re import timeimport os import argparsedef follow(thefile):thefile.seek(0, os.SEEK_END)while True:_line thefile.readline()if not _line:time.sleep(0…

RabbitMQ 搭建和工作模式

MQ基本概念 1. MQ概述 MQ全称 Message Queue&#xff08;[kjuː]&#xff09;&#xff08;消息队列&#xff09;&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 &#xff08;队列是一种容器&#xff0c;用于存放数据的都是容器&#xff0…

docker部署微服务

目录 docker操作命令 镜像操作命令 拉取镜像 导出镜像 删除镜像 加载镜像 推送镜像 部署 pom文件加上 在每个模块根目录加上DockerFile文件 项目根目录加上docker-compose.yml文件 打包&#xff0c;clean&#xff0c;package 服务器上新建文件夹 测试docker-compo…

基于springboot和微信小程序的流浪动物管理系统

基于springboot和微信小程序的流浪动物管理系统 内容简介 基于微信小程序实现的流浪动物管理系统&#xff0c;该系统针对用户与管理员两种角色进行开发。 1、提供流浪动物的信息查询功能&#xff0c;包括品种、年龄、性别、健康状况等&#xff0c;提供救助活动报名功能。 2…

5.1 PBR基础 BRDF介绍

基于物理的渲染&#xff08;Physically Based Rendering&#xff0c;PBR&#xff09;是指使用基于物理原理和微平面理论建模的着色/光照模型&#xff0c;以及使用从现实中测量的表面参数来准确表示真实世界材质的渲染理念。 一、反射率方程 理论基础放在参考链接里。 直接开始…

【uniapp】uniapp开发小程序定制uni-collapse(折叠面板)

需求 最近在做小程序&#xff0c;有一个类似折叠面板的ui控件&#xff0c;效果大概是这样 代码 因为项目使用的是uniapp&#xff0c;所以打算去找uniapp的扩展组件&#xff0c;果然给我找到了这个叫uni-collapse的组件&#xff08;链接&#xff1a;uni-collapse&#xff09…

超详细的接口测试

本文主要分为两个部分&#xff1a; 第一部分&#xff1a;主要从问题出发&#xff0c;引入接口测试的相关内容并与前端测试进行简单对比&#xff0c;总结两者之前的区别与联系。但该部分只交代了怎么做和如何做&#xff1f;并没有解释为什么要做&#xff1f; 第二部分&#xf…

ShellCode漏洞

ShellCode漏洞 可以查看如下网址&#xff1a; https://www.cnblogs.com/kakadewo/p/12996878.html 定义&#xff1a; shellcode是一段用于利用软件漏洞而执行的代码&#xff0c;shellcode为16进制之机械码&#xff0c;以其经常让攻击者获得shell而得名。shellcode常常使用机…

老鸟总结,软件测试工程师职业发展规划路线,入门到冲击大厂...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试工程师发展…

YOCTO 下载repo工具失败解决办法

curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o repocp repo ~/binchmod ax ~/bin/repo如果使用时报错&#xff0c; 切换ubuntu 到 python3 版本。gedit repo 修改repo默认链接地址&#xff1a;REPO_URL "https://gerrit.googlesource.com/git-repo"…

Spring AOP-面向切面编程概念

Spring AOP-面向切面编程概念 AOP&#xff08;面向切面编程&#xff09;是编程范式的一种&#xff0c;它允许程序员将横切关注点&#xff08;cross-cutting concerns&#xff09;模块化。在面向切面编程中&#xff0c;这些横切关注点通常体现为在多个点重复出现的代码&#xf…

Android设计模式--适配器模式

至诚之道&#xff0c;可以前知 一&#xff0c;定义 适配器模式把一个类的接口变换成客户端所期待的另一种接口&#xff0c;从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。 适配器模式在我们的开发中使用率极高&#xff0c;ListView&#xff0c;GridView&am…

面试cast:reinterpret_cast/const_cast/static_cast/dynamic_cast

目录 1. cast 2. reinterpret_cast 3. const_cast 3.1 加上const的情况 3.2 去掉const的情况 4. static_cast 4.1 基本类型之间的转换 4.2 void指针转换为任意基本类型的指针 4.3 子类和父类之间的转换 5. dynamic_cast 5.1 RTTI(Run-time Type Identification) 1.…

Selenium实现多页面切换

当使用 Selenium 进行自动化测试或爬取数据时&#xff0c;有时需要处理多个页面之间的切换。以下是一些可能需要多页面切换的情况&#xff1a; 1、打开新窗口/页面&#xff1a; 在当前页面上点击链接、按钮或执行某些操作时&#xff0c;可能会打开一个新的窗口或页面。此时&a…

【element优化经验】怎么让element-ui中表单多语言切换排版不乱

目录 前言&#xff1a; 痛点&#xff1a; 1.左对齐&#xff0c;右对齐在中文和外语情况下字数不同&#xff0c;固定宽度会使名称换行&#xff0c;不在整行对齐&#xff0c;影响美观。 2.如果名称和输入框不在一行&#xff0c;会使页面越来越长 3.label-width值给变量&#…

随笔记录-springmvc_ResourceHandlerRegistry+ResourceHttpRequestHandler

环境&#xff1a;springboot-2.7.5 配置文件配置静态资源映射 springboot配置静态资源映射方式是通过 WebMvcAutoConfiguration 实现的 spring: # resources: # # 自springboot 2.5.5之后&#xff0c;该属性已经被废弃&#xff0c;使用spring.web.resources.static-locat…

爬虫逆向你应该懂得Javascript知识

背景 大家在学习爬虫逆向的时候&#xff0c;一般都会涉及到对js源文件进行代码扣去&#xff0c;但是有的时候&#xff0c;你最好有js基础&#xff0c;能发现加密或者解密在那个位置&#xff0c;或者是能用python改写js代码&#xff0c;这就对个人的Javascript的能力有一定要求…