C++ 面试必问:深入理解虚函数表

article/2025/10/4 5:30:51

点击蓝字

31f477efddaa543bede5580458928fbb.png

关注我们

深入理解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测试:

043cf222bc66f88cbd32e413be0ecd76.png

d0831c5f26590a60c5f375e69b794815.png


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

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

单继承下的虚函数表

派生类未覆盖基类虚函数

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

390fa46f1e8e6fe7cd816e5ed8ca1add.png


可以看到下面几点:

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;
}

测试效果:

83499b09944e972e85ff940f72345af0.png


派生类覆盖基类虚函数

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

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

  • 派生类没有覆盖的虚函数延用基类的

测试代码:

#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;
}

测试效果:

daa89ddf6db182b0a78035c20f0b1b08.png

ba045c654172839f4d43d4bbb5418531.png

多继承下的虚函数表

无虚函数覆盖

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

  • 每个基类都有自己的虚函数表

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

93f41a317f8ddd6308e12d95b064e056.png


测试代码

#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;
}

测试效果:

2cf91b9ce465926909407d05216921f2.png


派生类覆盖基类虚函数

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

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数延用基类的

代码如下所示,注意这里只给出了类的定义,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;
};

测试效果

710094f7239959d278a510b38439606d.png

钻石型虚继承

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

测试代码

// 测试四:钻石型虚继承//虚基指针所指向的虚基表的内容:
//	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>

总结

几个原则

单继承

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

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

安全性问题

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

*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。

2789d9c50167c1767b3d2875567e946a.png

4c3d2eff4b4f2c0892337fba196c78e7.gif

戳“阅读原文”我们一起进步


http://chatgpt.dhexx.cn/article/9QePqBpg.shtml

相关文章

C++ 虚函数表

C类在内存中的存储方式 C 内存分为 5 个区域&#xff1a; 堆 heap &#xff1a;由 new 分配的内存块&#xff0c;其释放编译器不去管&#xff0c;由程序员自己控制。如果程序员没有释放掉&#xff0c;在程序结束时系统会自动回收。涉及的问题&#xff1a;“缓冲区溢出”、“内…

ThinkPHP3.2.2获取数据列getField()优化

getField()是一个常用方法&#xff0c;我习惯用来获取带key的数组&#xff0c;方便数据整合。 使用第1个参数&#xff0c;传入一个字段名&#xff0c;获取某一个数据值&#xff0c;返回满足条件的数据表中的该字段的第一行的值&#xff1a; $id M("User")->getFi…

java class getfield_java.lang.Class.getField()方法实例

全屏 java.lang.Class.getField() 返回一个Field对象&#xff0c;它反映此Class对象所表示的类或接口的指定公共成员字段。 name参数是一个字符串&#xff0c;指定所需字段的简单名称。 声明 以下是java.lang.Class.getField()方法的声明public Field getField(String name) th…

java getfield_Java 反射:通过 getField() 设置公共全局变量

Java 通过 getField() 操作公共全局变量 以前写 JavaWeb 项目启动初始化系统配置全局变量的代码&#xff0c;都是 variable Properties.getProperty(name) 这样一行一行代码的设置&#xff0c;变量少还好说&#xff0c;变量一多真的很磨叽。所以一直想通过 循环 简化代码&…

getField和getDeclaredField的区别

这两个方法都是用于获取字段 1.getField 只能获取public的&#xff0c;包括从父类继承来的字段。 2.getDeclaredField 可以获取本类所有的字段&#xff0c;包括private的&#xff0c;但是不能获取继承来的字段。 (注&#xff1a; 这里只能获取到private的字段&#xff0c;但并不…

vscode插件开发总结

一、关于vscode插件 相信大家对vscode应该都不陌生&#xff0c;VSCode是微软出的一款轻量级代码编辑器&#xff0c;免费而且功能强大&#xff0c;以功能强大、提示友好、不错的性能和颜值俘获了大量开发者的青睐&#xff0c;对JavaScript和NodeJS的支持非常好&#xff0c;自带…

2021-前端-VsCode插件

此乃吾习前端&#xff0c;VsCode之插件&#xff0c;个人所装&#xff0c;喜着自拿&#xff0c;不足之处还望海涵&#xff0c;多加批评。 1.Auto Close Tag——自动闭合尾部的标签 2.Atuo Rename Tag——修改 html 标签 自动帮你完成头部和尾部闭合标签的同步修改 3.Bracket…

关于VSCode插件的安装位置

VSCode的插件地址修改_上善若泪-CSDN博客_vscode插件位置文章目录1 data文件夹2 使用--extensions-dir命令3 使用mklink命令vscode编辑器强大的地方是可以使用各种各样的插件&#xff0c;但是插件默认的地方是在:C盘&#xff0c;让一些强迫症可能会受不了&#xff0c;非要迁移到…

vscode 插件-常用插件

VSCode常用插件(安装步骤同汉化) 1、*Auto Close Tag (自动闭合HTML/XML标签) 2、*Auto Rename Tag (自动帮你完成尾部闭合标签的同步修改&#xff0c;不过有些bug) 3、*Prettier(Prettier 是目前 Web 开发中最受欢迎的代码格式化程序) 安装Prettier -Code formatter这个插件…

Vscode 插件包下载并离线安装

打开VSCode插件官网 官网链接是https://marketplace.visualstudio.com/vscode 搜索Go 在输入框中输入go&#xff0c;搜索&#xff0c;结果如下&#xff1a; 点击Download Extension下载 注意&#xff1a;有时候找不到Download Extension&#xff0c;可能是网速加载慢&…

VsCode插件安装及推荐

1、快捷键Ctrl P&#xff0c;打开插件&#xff0c;输入 ext install &#xff08;我习惯的输入方式&#xff09;&#xff1b; 2、或者点击图片中的圈红的按钮&#xff0c;也可以进入插件安装商城&#xff1b; 3、下面开始说下我目前安装的插件&#xff08;我目前是vue开发&…

VScode安装离线插件

1. 下载及安装 首先在VScode官方插件库下载自己所需要的插件&#xff1a;https://marketplace.visualstudio.com/vscode 下载成功之后是以**.vsix**结尾的文件 然后再VScode软件中进行导入刚下载的文件 如果提示蓝色信息则为安装成功&#xff0c;红色则为失败 2. 版本不兼容报…

VSCode前端必备插件

跨平台的文本编辑器。由于其卓越的性能和丰富的功能&#xff0c;它很快就受到了大家的喜爱。 就像大多数 IDE 一样&#xff0c;VSCode 也有一个扩展和主题市场&#xff0c;包含了数以千计质量不同的插件。为了帮助大家挑选出值得下载的插件&#xff0c;我们针对性的收集了一些…

如何写一个vscode插件

1.运行yo code创建项目 2.选择使用yarn或者npm 3.运行 官网这个例子需要我们 ctrl shirt p 调出输入框, 然后在里面输入hello w 就可以如图所示 activationEvents: 当什么情况下, 去激活这个插件 activationEvents.onCommand: 在某个命令下激活(之后会专门列出很多其他条件…

Python好用的VSCode插件

1. Better Comments 这是一个让你能更好地编写注释的工具&#xff0c;它能根据关键词用不同的颜色高亮代码片段。支持以下类型的高亮&#xff1a; 感叹号 “!” 代码警告。问号“?”代表存留疑问。TODO 代码未来将要进行的操作。param 参数 2. autoDocstring 能够自动生成函…

VScode插件(自用)

一、Material Icon Theme 图标插件 它采用了 Google Material Design 风格&#xff0c;文件图标以及文件夹图标覆盖非常的全面。 二、 颜色主题插件Themes&#xff08;代码颜色&#xff09;中的 Monokai Dimmed 三、css peek 使用此插件&#xff0c;你可以追踪至样式表中 CSS…

2022年好用的Vscode插件

Chinese(VSCode汉化插件) 第一款推荐的插件叫Chinese 是一款VSCode汉化插件 这样&#xff0c;VSCode就完成了汉化 Material Theme(主题插件) 第二款插件是一个好看的主题插件Material Theme 它包含了多套不同色彩风格的主题&#xff0c;以及好看的图标样式。 选择不同主题可以…

VSCode 插件

文章目录 VSCode 插件 VSCode 插件 图标插件作用Auto Import在j\ts文件中&#xff0c;直接使用外部依赖包的变量名&#xff0c;此时&#xff0c;会自动写入导入语句Bracket Pair Colorizer2VS Code 已经内置Chinese (Simplified) (简体中文)汉化Code Spell Checker适用于代码和…

vscode常用插件总结

1、Code Spell Checker 检查单词拼写 2、Auto Rename Tag html/xml标签改变&#xff0c;将会成对改变&#xff5e; 3、Color Highlight 颜色标记 4、vscode-icon 文件图标 5、Turbo Console Log 或者 javascript console utils 按ctrlAltL可以快速输出console.log 6、v…

vscode插件(个人正在用的)

插件目录 any-ruleAuto Close TagAuto Rename Tagbackground-coverChinese (Simplified) (简体中文) Language Pack for Visual Studio CodeDebugger for JavaError LensESLintExtension Pack for JavaImage previewIntelliCodeIntelliCode API Usage ExamplesLanguage Support…