C++ 虚函数表剖析

点击蓝字

0101cc8d3792042f3bb79882d5861470.png

关注我们

一、概述

为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。

二、类的虚表

每个包含了虚函数的类都包含一个虚表。

我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:  virtual void vfunc1();  virtual void vfunc2(); void func1();  void func2();
private:  int m_data1, m_data2;  
};

类A的虚表如图1所示。

5c6a39b00db4be32948966c5d6f8c6e5.jpeg
图1:类A的虚表示意图

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。

虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

三、虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

e68e79f70d864d5f31a831c142813fbf.jpeg
图2:对象与它的虚表

上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

四、动态绑定

说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

class A {
public:   virtual void vfunc1();  virtual void vfunc2();  void func1();   void func2();
private:  int m_data1, m_data2;  
};
class B : public A {
public: virtual void vfunc1();   void func1();
private:  int m_data3;  
};
class C: public B {
public:  virtual void vfunc2();  void func2();
private:  int m_data1, m_data4;  
};

类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图3所示。

b6bc04afad62d4120e30f232e10de5d0.jpeg
图3:类A,类B,类C的对象模型

由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()A::vfunc2()

类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()A::vfunc2()

类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()

虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。

非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

假设我们定义一个类B的对象bObject。由于bObject是类B的一个对象,故bObject包含一个虚表指针,指向类B的虚表。

int main() {  B bObject;  
}

现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。

int main()   
{   B bObject;   A *p = & bObject;  
}

当我们使用p来调用vfunc1()函数时,会发生什么现象?

int main()
{  B bObject;  A *p = & bObject;  p->vfunc1();  
}

程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。

首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。

然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。

最后,根据虚表中找到的函数指针,调用函数。从图3可以看到,B vtbl的第一项指向B::vfunc1(),所以p->vfunc1()实质会调用B::vfunc1()函数。

如果p指向类A的对象,情况又是怎么样?

int main() 
{  A aObject;  A *p = &aObject;  p->vfunc1();  
}

aObject在创建时,它的虚表指针__vptr已设置为指向A vtbl,这样p->__vptr就指向A vtbl。vfunc1在A vtbl对应在条目指向了A::vfunc1()函数,所以p->vfunc1()实质会调用A::vfunc1()函数。

可以把以上三个调用函数的步骤用以下表达式来表示:

(*(p->__vptr)[n])(p)

可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。

我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。

那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。

  • 通过指针来调用函数

  • 指针upcast向上转型(继承类向基类的转换称为upcast,关于什么是upcast,可以参考本文的参考资料)

  • 调用的是虚函数

如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。

五、总结

封装,继承,多态是面向对象设计的三个特征,而多态可以说是面向对象设计的关键。C++通过虚函数表,实现了虚函数与对象的动态绑定,从而构建了C++面向对象程序设计的基石。

参考资料

  • 《C++ Primer》第三版,中文版,潘爱民等译

  • http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/

  • 侯捷《C++最佳编程实践》视频,极客班,2015

  • Upcasting and Downcasting, http://www.bogotobogo.com/cplusplus/upcasting_downcasting.php

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

1d041e44a523a1d226dd9c4437133fad.png

d96e5556dbd69a13bdabd8770875bb59.gif

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

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

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

相关文章

aix pax_通过Pax考试对JBoss Fuse 6.x进行集成测试,第一部分

aix paxJBoss Fuse是一个功能强大的分布式集成平台,具有内置功能,可用于针对集成的微服务部署进行集中式配置管理,服务发现,版本控制,API网关,负载平衡,故障转移等。 JBoss Fuse 6.x构建在Fabri…

android王者调不了界面,王者荣耀登录界面怎么改?登录界面更改教程[多图]

王者荣耀登录界面怎么改?许多玩家都想更改自己登陆的界面,但是都不清楚,下面就让安卓乐园小编为大家带来,登录界面更改教程。王者荣耀登录界面怎么改?1、安卓手机打开文件管理,找到根目录下Android/data/co…

y空间兑换代码_Python爬虫实战:QQ空间全自动点赞工具

QQ空间自动点赞前景提要目标确定分析介绍登陆获取cookie寻找XML寻找可变参数获取第一个空间动态寻找点赞所需的URL寻找可变参数功能提升到秒赞全部代码最后还是希望你们能给我点一波小小的关注。奉上自己诚挚的爱心私信小编01即可获取大量Python学习资料前景提要因为我周围的小…

学点 STL C++ 无序容器和元组

点击蓝字关注我们无序容器我们已经熟知了传统 C 中的有序容器 std::map/std::set&#xff0c;这些元素内部通过红黑树进行实现&#xff0c; 插入和搜索的平均复杂度均为 O(log(size))。在插入元素时候&#xff0c;会根据 < 操作符比较元素大小并判断元素是否相同&#xff0c…

html中的文档格式及举例,跟我一起从零开始学习WebAssembly(三)、最简单的例子hello world(使用自定义HTML模板)...

文章目录创建C代码片创建我们的自定义HTML模板文件编译运行实例有时我们想要使用我们自定义HTML模板。让我们来看看我们如何做到这一点。创建C代码片首先&#xff0c;创建一个名为hello2的目录。其次&#xff0c;在该目录下创建一个名为hello2.c文件。并将以下C代码保存在文件中…

设计模式适配器模式_21世纪的设计模式:适配器模式

设计模式适配器模式这是我的演讲的第三部分&#xff0c;“ 21世纪的设计模式” 。 适配器模式桥接世界。 在一个世界中&#xff0c;我们有一个概念的界面。 在另一个世界&#xff0c;我们有不同的界面。 这两个接口有不同的用途&#xff0c;但有时我们需要进行转移。 在编写良…

excel单元格斜线_掌握这20个Excel技巧,小白轻松变大神

掌握一些Excel小技巧&#xff0c;可以让你的工作效率翻倍&#xff0c;原本半个小时才能搞定的&#xff0c;现在几秒就可以轻松搞定。1、调整单元格大小选中表格&#xff0c;将光标移到表格顶部边框处&#xff0c;等其变成双向箭头即可移动。2、快速插入空行选中行&#xff0c;按…

html鼠标滚动效果代码,JS+CSS实现大气清新的滑动菜单效果代码

本文实例讲述了JSCSS实现大气清新的滑动菜单效果代码。分享给大家供大家参考&#xff0c;具体如下&#xff1a;这是一款比较大气清新的滑动导航菜单&#xff0c;CSS和JavaScript配合完成&#xff0c;鼠标放到一级菜单上&#xff0c;会滑出二级的菜单&#xff0c;兼容性也不错&a…

docker和java容器_使用Docker容器和Java EE进行持续交付

docker和java容器组织需要一种使应用程序交付快速&#xff0c;可预测和安全的方法&#xff0c;而诸如docker之类的容器所提供的敏捷性则可以帮助开发人员实现这一目标。 对于Java EE应用程序&#xff0c;这可以在容器中打包应用程序&#xff0c;应用程序服务器和其他依赖项&…

alientek ministm32液晶显示程序_佳显12864中文字库液晶专业生产液晶显示模块

GDRAM&#xff1a;(Graphic Display RAM)&#xff1a;图形显示RAM&#xff0c;这一块区域用于绘图&#xff0c;往里面写啥&#xff0c;屏幕就会显示啥&#xff0c;它与DDRAM的区别在于&#xff0c;往DDRAM中写的数据是字符的编码&#xff0c;字符的显示先是在CGROM中找到字模&a…

C++ 面试考点(一)

点击蓝字关注我们C 基础1、引用和指针的区别&#xff1f;初始化:引用在定义的时候必须进行初始化&#xff0c;并且不能够改变指针在定义的时候不一定要初始化&#xff0c;并且指向的空间可变访问逻辑不同:通过指针访问对象, 用户需要使用间接访问通过引用访问对象, 用户只需使用…

dojo还有人用吗_我的Dojo中有一个Mojo(如何编写Maven插件)

dojo还有人用吗我一直忙于在工作中使用Maven的腋窝。 对于很多开发人员&#xff0c;我会听到&#xff1a;“那又怎样。” 区别在于&#xff0c;我通常在无法直接访问Internet的环境中工作。 因此&#xff0c;当我说我经常使用Maven时&#xff0c;这意味着某些事情。 依赖地狱 …

html5 css3炫酷效果,28种纯CSS3炫酷loading加载动画特效

这是一组效果非常炫酷的纯CSS3 Loading加载动画特效。这组loading动画共有27种不同的效果。每一种loading动画都是通过CSS3的keyframes帧动画来完成的&#xff0c;每一个加载动画都构思新颖&#xff0c;效果非常的酷。安装可以通过bower来按钮这个loading动画特效&#xff1a;b…

中点和中值滤波的区别_频谱仪和EMI测试接收机什么区别?安泰维修中心分享

测试人员在选择使用射频仪器的时候都在纠结选择频谱仪还是测试接收机又或者信号分析仪。下面由安泰频谱分析仪维修中心分享频谱仪和EMI测试接收机什么区别&#xff1f;测量接收机是什么&#xff1f;频谱仪和信号分析仪什么区别&#xff1f;信号源分析仪是什么&#xff1f;一、频…

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

点击蓝字关注我们深入理解C 虚函数表C中的虚函数的作用主要是实现了多态的机制。关于多态&#xff0c;简而言之就是用父类型别的指针指向其子类的实例&#xff0c;然后通过父类的指针调用实际子类的成员函数。Derive d; Base1 *b1 &d; Base2 *b2 &d; Base3 *b3 &…

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!》(分…

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

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

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…