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

点击蓝字

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://www.mzph.cn/news/337688.shtml

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

相关文章

html 图片剪裁压缩,HTML5 canvas实现图片拉伸、压缩与裁剪

前言&#xff1a;我们在网页中经常会用到图片展示&#xff0c;通常情况下会给一个固定的宽高来显示这个图片&#xff0c;然而从服务器端上传的图片大小是不确定的&#xff0c;如果直接按默认填充这个框有时候就会特别丑orz。作为一个完(wai)美(mao)主(xie)义(hui)者&#xff0c…

switch日文键盘打中文_12月有哪些Switch游戏值得期待?

文章转自A9vg&#xff0c;作者setsuka_duki 经历了11月的游戏浪潮后&#xff0c;一年中最后一个月份也悄然而至&#xff0c;相比较“战火连天”的11月&#xff0c;在12月发售的重量级游戏并不算太多&#xff0c;这边为大家整理12月哪些值得一玩的Switch游戏。 《Tools Up!》(分…

neo4j cypher_Neo4j:使用Cypher生成实时建议

neo4j cypherNeo4j的最常见用途之一是构建实时推荐引擎&#xff0c;一个共同的主题是它们利用大量不同的数据来提出有趣的推荐。 例如&#xff0c; 在此视频中&#xff0c; 阿曼达&#xff08;Amanda&#xff09;展示了约会网站如何通过社交联系开始&#xff0c;然后介绍热情&…

基于 C++11 的线程池 threadpool , 简洁且可以带任意多的参数

点击蓝字关注我们咳咳。C11 加入了线程库&#xff0c;从此告别了标准库不支持并发的历史。然而 c 对于多线程的支持还是比较低级&#xff0c;稍微高级一点的用法都需要自己去实现&#xff0c;譬如线程池、信号量等。线程池(thread pool)这个东西&#xff0c;在面试上多次被问到…

html jq移到出现内容,jquery操作html元素之( 获得内容和属性)

jQuery DOM 操作jQuery 中非常重要的部分&#xff0c;就是操作 DOM 的能力。jQuery 提供一系列与 DOM 相关的方法&#xff0c;这使访问和操作元素和属性变得很容易。提示&#xff1a;DOM Document Object Model(文档对象模型)DOM 定义访问 HTML 和 XML 文档的标准&#xff1a;…

c# 字典排序_Python零基础入门之列表与字典

本篇内容需结合源码&#xff0c;获取方法看末尾数据结构数据结构就是指从计算机存储、组织数据的结构列表(List) 元组(Tuple)字典(Dictionary)集合(Set)列表(List)列表中的数据按顺序排列列表有正序与倒序两种索引列表可存储任意类型数据&#xff0c;且允许重复创建列表变量名 …

jaxb xsd生成xml_使用JAXB和Jackson从XSD生成JSON模式

jaxb xsd生成xml在本文中&#xff0c;我演示了一种从XML Schema &#xff08;XSD&#xff09;生成JSON Schema的 方法 。 在概述从XML Schema创建JSON Schema的方法的同时&#xff0c;本文还演示了JAXB实现的使用&#xff08;与JDK 9捆绑在一起的xjc版本2.2.12-b150331.1824 [b…

C语言中结构体struct的用法

点击蓝字关注我们定义结构体变量下面举一个例子来说明怎样定义结构体变量。struct string { char name[8]; int age; char sex[2]; char depart[20]; float wage1, wage2, wage3, wage4, wage5; }person;这个例子定义了一个结构名为string的结构体变量person。还可以省略变量名…

html全屏漂浮,jquery全屏漂浮广告插件,可点击关闭(原创)

jquery全屏漂浮广告插件 兼容主流浏览器 实现简单 调用简单 在线预览 源码下载jquery全屏漂浮广告插件 兼容主流浏览器 实现非常简单说明&#xff1a;jquery在1.9开始不支持.live()方法的写法而改用.on()&#xff0c;见http://jquery.com/upgrade-guide/1.9/#live-removed.该插…

华南理工计算机接口技术随堂练习_研究生考试计算机408跟845有什么区别?

楼上已经解释了什么是408&#xff0c;什么是845&#xff0c;那我当时考的就是计算机408&#xff0c;我就来补充一下408该怎么复习才能拿高分吧。首先介绍一下我自己&#xff0c;计算机本科生&#xff0c;然后头铁考了某个985的计算机专业&#xff0c;最后以403分的成绩成功上岸…

关于RabbitMQ集群分区的通知

如果您在集群中运行RabbitMQ&#xff0c;则集群不太可能会被分区 &#xff08;集群的一部分失去与其余部分的连接&#xff09;。 上面的链接页面介绍了显示状态和配置行为的基本命令。 当发生分区时&#xff0c;您首先希望得到通知&#xff0c;然后再解决它。 RabbitMQ实际上使…

C++ 面试考点(二)

点击蓝字关注我们11、extern 用法&#xff1f;extern 修饰变量的声明如果文件a.c 需要引用b.c 中变量int v&#xff0c;就可以在a.c 中声明extern int v&#xff0c;然后就可以引用变量v。extern 修饰函数的声明如果文件a.c 需要引用b.c 中的函数&#xff0c;比如在b.c 中原型是…

内存不能为read进不去桌面_四级报名进不去怎么办

英语四级报名进不去怎么办?这里提供有两种方法&#xff0c;一种是重复刷新&#xff0c;直到页面出现;另外一种就是错峰报名&#xff0c;叉开登录高峰期。很多考生在报名的时候遇到困难&#xff0c;最多的就是报名页面进不去&#xff0c;这个时候有一些考生就会产生疑惑&#x…

bean创建异常_快速提示:消息驱动Bean中的异常处理

bean创建异常让我们快速回顾一下关于消息驱动Bean的特殊处理。 MDB的入口点是重写的onMessage方法。 它不提供引发检查异常的作用域&#xff0c;因此&#xff0c;如果要处理错误情形&#xff0c;则需要从代码中传播未检查异常&#xff08;java.lang.RuntimeException的子类&am…

C语言线程库的使用,这篇值得收藏!

点击蓝字关注我们1. 线程概述线程是轻量级的进程&#xff08;LWP&#xff1a;light weight process&#xff09;&#xff0c;在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合&#xff0c;指令按照既定的逻辑控制计算机运行。操作系统会以进…

alexeyab darknet 编译_【目标检测实战】Darknet—yolov3模型训练(VOC数据集)

原文发表在&#xff1a;语雀文档0.前言本文为Darknet框架下&#xff0c;利用官方VOC数据集的yolov3模型训练&#xff0c;训练环境为&#xff1a;Ubuntu18.04下的GPU训练&#xff0c;cuda版本10.0&#xff1b;cudnn版本7.6.5。经过一晚上的训练&#xff0c;模型20个类别的mAP达到…

html字符串转svg,【SVG】如何操作SVG Text

上周我们学习了如何使用元素创建SVG文本。在实例中我们设置了x和y坐标来定位文本&#xff0c;也尝试了给SVG文本中的每个字符定位。关于元素还有很多内容。在处理SVG文本时&#xff0c;不要局限于x和y属性。元素还有几个可以添加的属性&#xff0c;现在我们开始讨论吧。dx和dy属…

aws sqs_在Spring使用AWS SQS创建消息驱动Bean

aws sqs在我之前的文章中&#xff0c;我展示了一个简单的示例&#xff0c;该示例如何将AWS SQS与Spring Framework结合使用以将消息放入队列并从队列中读取消息。 在本文中&#xff0c;我将更进一步&#xff0c;并使用Spring创建一个“消息驱动的Bean”&#xff0c;以便对放入队…

C++ 面试考点(三)

点击蓝字关注我们21、构造函数和析构函数可以调用虚函数吗&#xff0c;为什么在C中&#xff0c;提倡不在构造函数和析构函数中调用虚函数&#xff1b;在构造函数和析构函数调用的所有函数(包括虚函数)都是编译时确定的, 虚函数将运行该类中的版本.因为父类对象会在子类之前进行…

用终端访问路由器设置端口开发_serial for mac(终端管理软件)v2.0.3

原标题&#xff1a;serial for mac(终端管理软件)v2.0.3serial for mac是应用在Mac上的一款终端管理软件&#xff0c;可以帮助您连接和控制串行设备&#xff0c;如服务器&#xff0c;路由器或调制解调器等网络设备&#xff0c;PBX系统等。好消息是Serial为大多数串行设备提供了…