深入理解C++ 虚函数表

目录

  • 深入理解C++ 虚函数表
    • 虚函数表概述
    • 单继承下的虚函数表
      • 派生类未覆盖基类虚函数
      • 派生类覆盖基类虚函数
    • 多继承下的虚函数表
      • 无虚函数覆盖
      • 派生类覆盖基类虚函数
    • 钻石型虚继承
    • 总结
      • 几个原则
      • 安全性问题

深入理解C++ 虚函数表

​ C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

​ 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

本文将详细介绍虚函数表的实现及其内存布局。

虚函数表概述

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。

C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

按照上面的说法,来看一个实际的例子:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};int main()
{Base t;(     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 2))   )     ();return 0;
}

经过VS2017,x86测试:

1558792775022

1558844330501

我们成功地通过实例对象的地址,得到了对象所有的类函数。

1558793497748

main定义Base类对象t,把&b转成int *,取得虚函数表的地址vtptr就是:(int*)(&t),然后再解引用并强转成int *得到第一个虚函数的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二个虚函数g()的地址就是(int*)(*((int*)&t)) + 1,依次类推。

单继承下的虚函数表

派生类未覆盖基类虚函数

下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。

1558796143678

可以看到下面几点:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面

测试代码:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Devired :public Base{
public:virtual void x() { cout << "x()" << endl; }
};int main()
{Devired t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

测试效果:

1558798773222

派生类覆盖基类虚函数

再来看一下派生类覆盖了基类的虚函数的情形,可见:

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义)
  2. 派生类没有覆盖的虚函数延用基类的

测试代码:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Derive :public Base{
public:virtual void x() { cout << "x()" << endl; }virtual void f() { cout << "Derive::f()" << endl; }
};int main()
{Derive t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

测试效果:

1558844866127

1558844940167

多继承下的虚函数表

无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:

  1. 每个基类都有自己的虚函数表
  2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同),具体见下图所示:

img

测试代码

#include <iostream>
class Base
{
public:Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }private:int m_iMem1;int m_iMem2;
};class Base2
{
public:Base2(int mem = 3) : m_iBase2Mem(mem) { ; }virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }private:int m_iBase2Mem;
};class Base3
{
public:Base3(int mem = 4) : m_iBase3Mem(mem) { ; }virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }private:int m_iBase3Mem;
};class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }private:int m_iMem1;
};int main()
{// Test_3Devired d;int *dAddress = (int*)&d;typedef void(*FUNC)();/* 1. 获取对象的内存布局信息 */// 虚表地址一int *vtptr1 = (int*)*(dAddress + 0);int basemem1 = (int)*(dAddress + 1);int basemem2 = (int)*(dAddress + 2);int *vtpttr2 = (int*)*(dAddress + 3);int base2mem = (int)*(dAddress + 4);int *vtptr3 = (int*)*(dAddress + 5);int base3mem = (int)*(dAddress + 6);/* 2. 输出对象的内存布局信息 */int *pBaseFunc1 = (int *)*(vtptr1 + 0);int *pBaseFunc2 = (int *)*(vtptr1 + 1);int *pBaseFunc3 = (int *)*(vtptr1 + 2);int *pBaseFunc4 = (int *)*(vtptr1 + 3);(FUNC(pBaseFunc1))();(FUNC(pBaseFunc2))();(FUNC(pBaseFunc3))();(FUNC(pBaseFunc4))();// .... 后面省略若干输出内容,可自行补充return 0;
}

测试效果:

1558845492296

派生类覆盖基类虚函数

我们再来看一下派生类覆盖了基类的虚函数的情形,可见:

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置
  2. 派生类没有覆盖的虚函数延用基类的

代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:

class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }private:int m_iMem1;
};

测试效果
1558849805007

钻石型虚继承

该继承还是遵循上述的所有原则,我们直接来测试。

测试代码

// 测试四:钻石型虚继承//虚基指针所指向的虚基表的内容:
//  1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
//  2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;class B
{
public:B() : _ib(10), _cb('B') {}virtual void f(){cout << "B::f()" << endl;}virtual void Bf(){cout << "B::Bf()" << endl;}private:int _ib;char _cb;
};class B1 : virtual public B
{
public:B1() : _ib1(100), _cb1('1') {}virtual void f(){cout << "B1::f()" << endl;}#if 1virtual void f1(){cout << "B1::f1()" << endl;}virtual void Bf1(){cout << "B1::Bf1()" << endl;}
#endifprivate:int _ib1;char _cb1;
};class B2 : virtual public B
{
public:B2() : _ib2(1000), _cb2('2') {}virtual void f(){cout << "B2::f()" << endl;}
#if 1virtual void f2(){cout << "B2::f2()" << endl;}virtual void Bf2(){cout << "B2::Bf2()" << endl;}
#endif
private:int _ib2;char _cb2;
};class D : public B1, public B2
{
public:D() : _id(10000), _cd('3') {}virtual void f(){cout << "D::f()" << endl;}#if 1virtual void f1(){cout << "D::f1()" << endl;}virtual void f2(){cout << "D::f2()" << endl;}virtual void Df(){cout << "D::Df()" << endl;}
#endif
private:int _id;char _cd;
};int main(void)
{D d;cout << sizeof(d) << endl;return 0;
}

测试效果

1>class D   size(52):
1>  +---
1> 0    | +--- (base class B1)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | _ib1
1>12    | | _cb1
1>      | | <alignment member> (size=3)
1>  | +---
1>16    | +--- (base class B2)
1>16    | | {vfptr}
1>20    | | {vbptr}
1>24    | | _ib2
1>28    | | _cb2
1>      | | <alignment member> (size=3)
1>  | +---
1>32    | _id
1>36    | _cd
1>      | <alignment member> (size=3)
1>  +---
1>  +--- (virtual base B)
1>40    | {vfptr}
1>44    | _ib
1>48    | _cb
1>      | <alignment member> (size=3)
1>  +---
1>
1>D::$vftable@B1@:
1>  | &D_meta
1>  |  0
1> 0    | &D::f1
1> 1    | &B1::Bf1
1> 2    | &D::Df
1>
1>D::$vftable@B2@:
1>  | -16
1> 0    | &D::f2
1> 1    | &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0    | -4
1> 1    | 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0    | -4
1> 1    | 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1>  | -40
1> 0    | &D::f
1> 1    | &B::Bf
1>

总结

几个原则

单继承

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置
  2. 派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

  1. 每个基类都有自己的虚函数表
  2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后

安全性问题

当我们直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,我们可以用对象的地址访问到各个子类的成员函数,就违背了C++语义,操作会有一定的隐患,当我们使用时要注意这些危险的东西!

参考:

https://coolshell.cn/articles/12165.html

https://jocent.me/2017/08/07/virtual-table.html

https://blog.csdn.net/lihao21/article/details/50688337

转载于:https://www.cnblogs.com/Mered1th/p/10924545.html

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

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

相关文章

Bootstrap简介

1.使用准备 1.1 Bootstrap的下载 http://www.bootcss.com&#xff0c;下载用于生产环境的Bootstrap即可。 1.2 Bootstrap包含的内容 ● 全局CSS&#xff1a;基本的 HTML 元素均可以通过 class 设置样式并得到增强效果&#xff1b;还有先进的栅格系统。 ● 组件&#xff1a;无数…

tomcat需要设置环境变量吗

tomcat是一款轻量级web应用服务器&#xff0c;安装的时候我们都是直接解压zip包&#xff0c;然后在bin目录下双击startup.bat就可以启动了&#xff08;当然&#xff0c;前提是本地要安装jdk并配置JAVA_HOME环境变量&#xff09; 所以我一直认为tomcat是不用配置环境变量的 但是…

钱荒下银行理财收益率角逐:邮储银行垫底

21世纪资管研究员松壑 由于银行理财的收益定价机制为设定预期收益率的“先行定价”&#xff0c;而银行对产品本金收益又保有或明或暗的兑付要求&#xff0c;其业绩往往在理财产品发行前就已决定。 因此&#xff0c;本次榜单根据已披露最高预期收益率&#xff08;下称收益率&a…

数据结构7.3_图的遍历

我们希望从图中某一顶点出发访遍图中其余顶点&#xff0c;且使每一个顶点仅被访问一次。 这一过程就叫做图的遍历。 图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。 然而&#xff0c;图的遍历要比树的遍历复杂得多。 因为图的任一顶点都可能和其余的顶…

HCL实验四

PC端配置&#xff1a;配置ip地址 配置网关 交换机配置&#xff1a;①创建VLAN system-view vlan 10 vlan 20 ②配置PC端接口 interface vlan-interface 10 ip add 192.168.10.254 24 interface vlan-interface 20 ip add 192.168.20.254 24 转载于:https://www.cnblogs.com/zy5…

程序员/设计师能用上的 75 份速查表

本文由 伯乐在线 - 黄利民 翻译自 designzum。欢迎加入 技术翻译小组。转载请参见文章末尾处的要求。75 份速查表&#xff0c;由 vikas 收集整理&#xff0c;包括&#xff1a;jQuery、HTML、HTML5、CSS、CSS3、JavaScript、Photoshop 、git、Linux、Java、Perl、PHP、Python、…

GWAS: 网页版的基因型填充(genotype imputation)

在全基因组关联分析中&#xff0c;处理芯片数据时&#xff0c;必须走的一个流程就是基因型数据填充&#xff08;imputation&#xff09;。 当然&#xff0c;如果你拿到的是全测序的数据&#xff0c;请忽略这一步。 下面直奔主题&#xff0c;怎么在网页版进行基因型填充。 1 进入…

腾讯CKV海量分布式存储系统

摘要&#xff1a;腾讯CKV&#xff0c;是腾讯自主研发的高性能、低延时、持久化、分布式KV存储服务。在腾讯的微信平台、开放平台、腾讯云、腾讯游戏和电商平台广泛使用&#xff0c;日访问量超过万亿次。本文将全面剖析CKV的实现原理和技术挑战。 与Memcached和Redis等开源NoSQ…

编程之法:面试和算法心得

《编程之法&#xff1a;面试和算法心得》高清中文版PDF 含书目录 下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Kcd2bRsIfhagKZR6NaOgXg 提取码&#xff1a;054s 《编程之法&#xff1a;面试和算法心得》高清中文版PDF高清中文版PDF 含书目录&#xff0c;36…

localStorage存、取数组

localStorage存储数组时需要先使用JSON.stringify()转成字符串&#xff0c;取的时候再字符串转数组JSON.parse()。 var arr[1,2,3,4];localStorage.setItem(key,arr);console.log(localStorage(key); //打印出字符串&#xff1a;1,2,3,4 正常存储&#xff1a;localStorage.setI…

Redis原理及拓展

Redis是单线程程序。单线程的Redis为何还能这么快&#xff1f; 1、所有的数据都在内存中&#xff0c;所有的运算都是内存级别的运算&#xff08;因此时间复杂度为O(n)的指令要谨慎使用&#xff09; 2、单线程操作&#xff0c;避免了频繁的上下文切换 3、多路复用&#xff08;非…

日常问题 - 远程服务器运行Tomcat出现卡顿阻塞

问题描述&#xff1a; 远程服务器Tomcat容器运行一个WEB项目&#xff0c;浏览器访问时&#xff0c;请求一直得不到响应&#xff0c;并且除此之外没有出现任何异常&#xff0c;像是被阻塞了。查看远程Tomcat窗口&#xff0c;也没有任何报错。鼠标在Tomcat窗口右键点击后&#xf…

给技术人上的管理课:平衡和集中

摘要&#xff1a;大中型团队管理是技术人转型的巨大挑战&#xff0c;这个阶段的管理工作&#xff0c;仍然可以归为技术范畴&#xff0c;依靠的大抵是管理人的筋肉力量。是否会管理&#xff0c;要看能否管好超出自己筋肉力量规模的团队。此中的关键&#xff0c;在于把握平衡和集…

理解分布式id生成算法--雪花算法(SnowFlake)

分布式ID生成算法的有很多种&#xff0c;Twitter的SnowFlake就是其中经典的一种。 注&#xff1a; 1B就是1个字节。Byte、KB、B、MB、GB之间的关系是&#xff1a;Bit——比特 &#xff1b; B ——字节&#xff1b;KB——千字节&#xff1b;MB——兆字节&#xff1b;GB——吉字节…

注解 @PostConstruct 与 @PreDestroy 详解及实例

简介 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Java EE5 引入了PostConstruct和PreDestroy这两个作用于Servlet生命周期的注解&#xff0c;实现Bean初始化之前和销毁之前的自定义操…

PHP 安装xdebug

xdebug官网: https://xdebug.org 安装步骤如下: 使用 phpinfo() 打印出PHP相关信息, 全选, 复制 打开 xdebug 网站: https://xdebug.org/wizard.php 在图中输入框中粘贴你复制的信息, 点击 Analyse my phpinfo() output 在结果中点击下载, 然后按照它提示的步骤进行操作即可…

分布式消息中间件 : Rocketmq

简述 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 分布式消息中间件&#xff0c;主要是实现分布式系统中解耦、异步消息、流量销锋、日志处理等场景。生产中用的最多的消息队…

消息中间件:RocketMQ 介绍(特性、术语、原理、优缺点、消息顺序、消息重复)

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 消息中间件的作用 1. 应用解耦 2. 异步处理 比如用户注册场景&#xff0c;注册主流程完成以后&#xff0c;需要调用邮件系统发送邮件…

使用 Intellij Idea 打包 java 工程为可执行 jar 包

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 其实还有个简单多了方法&#xff0c;见&#xff1a; 超简单方法&#xff1a; Intellij Idea 把 java 工程打成可运行的 jar 步骤&#x…

QuickStart系列:docker部署之Gitlab本地代码仓库

gitlab是可以在本地搭建的使用git作为源代码管理的仓库。 运行环境&#xff1a; win10vmware14docker7docker 1. 使用命令拉取镜像&#xff08;非必须&#xff0c;耗时比较久&#xff0c;这里以ce为准&#xff0c;ce是社区版&#xff0c;ee是企业版&#xff09;&#xff1a; do…