图解C++虚函数

2016年07月02日 17:47:17 海枫 阅读数:5181 标签: 虚函数c++g++对象模型C++虚函数更多

个人分类: C/C++/linux

版权声明:本文为博主原创文章,承蒙转载请注明作者和出处 https://blog.csdn.net/linyt/article/details/51811314

介绍

早在5年前写过《从汇编层面深度剖析C++虚函数》一文,介绍C++的虚函数表和调用过程。最近在看OSv操作系统代码,迫不得已看了C++11中的新语法,最后还是跳不出虚函数的五指山。本文尽量使用图来解释虚函数在类,继承,多继承各种场下的对象模型结构,以及虚函数实现多态绑定。

值得注意的是,不同编译器生成的对象结构和虚函数表稍为有一些不同,本文均采用gcc 5.3.0版本下的g++编译器作为研究对象。

普通类

object类的定义

class object {int a;int b;public:object(): a(0), b(1) {}virtual void f() {}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上述代码中定义object类,定义两个int成员,分别是a和b,然后定义了虚函数f。

object对象内存结构

下图是定义两个object对象o1和o2的内存结构。

所有带虚函数类对象的首4字节为虚函数表(vtable)指针,object也是如此。object对象的第一个4字节为vtable指针,它指向object全局的虚函数表,每个类只需一个vtable表即可。o1和o2共享一个虚函数表。 
虚函数表的内容依次是:object::f(),object::g()

接下来8个字节是意想不到的东西,那就是object类的type_info对象,这由编译器生成的对象结构,它与C++库中的type_info内存部局完全相同。 
g++编译器将类的type_info对象信息放到了vtable表的尾部。

调用虚函数过程

下面图片描述了object指针调用虚函数的过程。 

具体过程可解释如下:

  1. 从o对象中找到它的虚函数表地址
  2. 根据g函数在虚函数表中的offset,该函数地址
  3. 根据函数地址进行调用

获取type_info对象

C++的RTTI机制本该属于别一个话题,不适合在虚函数中谈论。但在具体实现过程中,编译器将它和vtable合并到一起,所以还在有必要简单讨论RTTI机制。

由于type_info信息也是放到vtable里面,那可以认为typeid操作符是虚函数一部分,它在vtable也有一个offset.

下面是object对象获取它的type_info引用的过程。

与其它虚函数调用类似,typeid返回的type_info对象就是vtable尾部的type_info对象。 
每个类只有一个type_inof对象,不能被修改,所以typeid操作符只能是返回const引用。

可以想象一下typeid(o).name()就是返回type_info对象是name成员指向的字符器串”6object”。

继承类

父类和子类定义

下面代码定义父类base和子类derive.

class base {int b;
public:virtual void f() {}virtual void g() {}
};class derive: public base {int d;
public:virtual void g() {}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

base对象和derive对象内存结构

derive子类重写了g()函数,所以它的vtable中的第二项为derive::g(),而f()函数没有重写,所以第一项仍然是base::f()函数。

多态的实现

我们经常看到这样的代码:

base *b = new derive();
b->g();
  • 1
  • 2

在b->g()调用过程中,调用的是derive::g()函数,而不是base::g(),是如何实现的呢?这其中的奥秘就是虚函数表中。详见下图。

b对象尽管是base*类型的,但它的地址跟new出来derive对象地址是同一个(后面多重继承例子中就不是这样子的了),所以在调用b->g()时,从vtable指向的虚函数中找第二项,它值为derive::g()函数的地址,所以最终调用的是derive::g()函数。

多重继承

多重继承是更复杂的一个场景,在多重继承的情况下,子类指针向基类指针转换时,它的地址是不一样的,所以编译必须生成一些额外代码来做地址转换。

多重继承类定义

class base1 {int b1;
public:virtual void f1() {}virtual void g1() {}
};class base2 {int b2;
public:virtual void f2() {}virtual void g2() {}
};class base3 {int b3;
public:virtual void f3() {}virtual void g3() {}
};class derive: public base1, public base2, pbulic base3 {int d;
public:virtual void f1() {}virtual void f2() {}virtual void f3() {}
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

基类的对象内存结构

下图分别定义base1, base2, base3基类对象,不需过多解释。

派生类的对象内存结构

从这个图开始,我们开始要烧脑了。下图是derive对象d的内存结构:

derive对象内存结构有以下几个特点:

  1. base1, base2, base3这3个基类依次排列,后面才是derive类新增的d成员
  2. derive对象有3个虚函数表指针(请注意不是1个了,这里面大有戏法) 
    3.derive对象有前8字节,也是base1基类所在坑的位置;它的vtable指针指向的虚函数表,供derive类型使用,也供base1类型使用。对于base1类型,只使用前两项。而derive类型,则使用更多项
  3. derive类的虚函数表中:前两项的排列是与base1完全一样的,而后面的derive::f2(),derive::f3(),则是dervice类重载base2/base3虚函数的总列表。
  4. derive另外两个虚函数表,以base2坑的虚函数表为例,它有两项。第一项是non-virtual thunk to derive::f2(),第二项是base2::g2()。因为derive类没有重写g2函数,所以第二项填base2::g2()是乎合理解的。而non-virtul thunk to derive::f2()这项我们后面会解释。
  5. 其它的-8和-16数字,估计是其它语法场景下有用,目前没有看到,可以先跳过它们。
  6. 另外在整个derive的虚函数表中,出现两次derive类的type_info指针,先忽略它们吧。

派生类向基类转换的秘密

也许你知道,派生类对象转基类对象转换之后,这两者的地址都是一样的,而在多重继承里面,这个结论就不对了。

从上面看到,派生类是将基类依次排列而成。所以派生类对象指针向第一个基类指针转换时,两者地址是一样的;而第二个和第三个基类对象指针转换时,它的地址就不一样的。请看下图:

派生类调用非重写函数

以这两行代码为例 
derive *d = new derive(); 
d->g2();

显然,derive没有重写g2()函数,所以它调用的是base2类的虚函数。 
其实,不管derive是否有重写g2函数,都是通过base2的虚函数表找出来的。具体过程如下图所示:

由于g2函数是最早是由base2类定义的,所以d->g2()调用时,先从d对象中的base2虚函数表,查找g2偏移量(值为4)的表项,再调用。

但这里有个细节一定要注意的是,base2::g2函数的this指针是base2 *类型的,而这里的d是derive*类型的,需要先将derive *指针转换成base2*指针。这个转换完成之后,指针值就增加8字节了。

多重继承下的多态实现

这里详细分析

base2 *b2 = new derive();
b2->f2();
  • 1
  • 2

是如何实现从基类到派生类f2()函数的调用。

b2指针已指向了derive对象的base2部分,然后b2->f2()从base2-vtable对应的虚函数表的第一项,找到了non-virtual thunk to derive::f2(),然后调用。

咦,这里不应该是derive::f2()吗,那个non-virtual thunk to derive::f2()是什么鬼?

答案是和this指针强相关

derive::f2()函数的this指针肯定是derive*类型的,而这里的b2是base2*类型,不能直接调用。

non-virtual thunk to derive::f2()代码其实是两行汇编,它完成出b2指针从base*类型转换成derive*类型的功能,也即地址减去8。

小结

其实我想只用图表将C++虚函数全部表达出来,但当我画出来之后,发现很多细节不用文字稍作说明,不是很难明白。

其实这里说的C++虚函数原理跟你之前了解的应该是一致的,只是很难技术细节你没有想过而已,但不管理怎么样,我们一起学习吧。

后继再跟大家分析,菱形继承和虚继承场景下,虚函数的技术细节。

  • 胆识与智慧: 博主,请问我可以转载你的这篇文章吗?(1个月前#2楼)查看回复(1)

  • Q943381546: 看了你的文档收获很多,个人认为有点小缺陷。“以这两行代码为例 derive *d = new derive(); d->g2(); 其实,不管derive是否有重写g2函数,都是通过base2的虚函数表找出来的。”如果是重新函数,根据c++函数查找优先级,找到的是d的g2函数,应该是用d指针直接调用g2,不会退化到父类指针。(10个月前#1楼)查看回复(1)

图解C++虚函数 虚函数表相关推荐

  1. 38.【C++ 虚函数 纯虚函数 虚基类 (最全详解)】

    虚函数.虚基类.纯虚函数 (一).虚函数 1.什么是虚函数: 2.虚函数的格式: 3.关于虚函数的注意事项: 4.虚函数的作用: 5.虚函数访问格式 6.虚函数的各种疑难杂症 [当指针是基类.但虚函数 ...

  2. 【C++】多态 - 虚函数/虚析构函数以及虚函数表

    什么是多态: 指不同对象收到相同消息时或相同对象收到不同消息时产生不同的动作. 这里先说下为什么会用到虚函数: 以下面的程序为例: 这个程序中,Carp是Fish的继承类,而Carp中覆盖了Swim这 ...

  3. 虚函数 虚继承 抽象类

    虚函数.纯虚函数.虚基类.抽象类.虚函数继承.虚继承 虚函数:虚函数是C++中用于实现多态(polymorphism)的机制.核心理念就是通过基类访问派生类定义的函数.是C++中多态性的一个重要体现, ...

  4. c语言中虚函数和纯虚函数,虚函数和纯虚函数的区别是什么?

    虚函数和纯虚函数的区别:1.纯虚函数只有定义,没有实现:而虚函数既有定义,也有实现的代码.2.包含纯虚函数的类不能定义其对象,而包含虚函数的则可以. 相关推荐:<C++视频教程> 虚函数( ...

  5. 虚函数 纯虚函数 虚基类说明

    原文:http://www.cnblogs.com/ms-frank/archive/2008/01/16/1041310.html 虚基类 在说明其作用前先看一段代码 [cpp] view plai ...

  6. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  7. 虚函数原理与虚函数表

    目录 一. 虚函数 二.虚函数原理与虚函数表 一. 虚函数 虚函数: 使用 virtual 关键字声明的函数,是动态多态实现的基础. 非类的成员函数不能定义为虚函数. 类的静态成员函数不能定义为虚函数 ...

  8. 虚函数,虚函数表,虚函数实现原理,虚函数实现机制,虚函数解决的问题

    虚函数 虚函数表 虚函数实现原理? 虚函数解决的问题?虚函数解决问题的实现机制? 虚函数模型的构建? 虚函数模型的应用?

  9. 虚函数、虚函数表、虚继承

    1.虚函数 虚函数的定义: 虚函数必须是类的 非静态成员函数(且非构造函数),其访问权限是public(可以定义为privateor proteceted, 但是对于多态来说,没有意义),在基类的类定 ...

最新文章

  1. Linux查看目录挂载点
  2. linux删除目录所有内容,如何从Linux上的目录中删除所有内容
  3. URLConnection
  4. Taro+react开发(31)同样的数据可以数组遍历
  5. ide快捷键_一款好用的IDE怎么可以没有代码提示?
  6. Bootstrap 导航条的组件
  7. 关于nutz跨服务器上传文件
  8. wordpress 固定链接变成html,wordpress固定链接只有默认能用,其他链接设置网页打不开怎么办...
  9. 让hover效果平滑过渡回初始状态?
  10. matlab3d绘图实例,matlab各种三维绘图及实例
  11. C#:异步编程和线程的使用(.NET 4.5 )
  12. 使用ExtJS创建前端WebQQ界面
  13. No handler found for OPTIONS /lca/lcaplan/lcaplanprocess/list
  14. 一般意义的resolution地图分辨率和比例尺理解
  15. Window10总是自动打开网络代理的解决方案法
  16. php实现短信找回密码,thinkphp5怎么调用云片接口实现发送短信验证码找回密码功能...
  17. Android获取WiFi名称/路由器AP地址总结
  18. Tensorflow2.6实现Unet结构神经网络(3D卷积)识别脑部肿瘤并实现模型并行
  19. 支付宝赚赏金的多种玩法(引流+变现日入200+)
  20. ubuntu20+PHP项目运行环境搭建

热门文章

  1. [总结]vue开发常见知识点及问题资料整理(持续更新)
  2. Java8新特性Optional、接口中的默认方法与静态方法
  3. C/C++ 电子书推荐
  4. [转]ASP.Net缓存总结
  5. CSS综合征病例,医药-churg-strauss 综合征 (css) 变应性嗜酸性肉芽肿.ppt
  6. 用matlab绘制抛物线y的x平方,我刚刚学习MATLAB,想画一下(x^2+y^2-1)^3=x^2*y^3这个曲线的图像,该怎么画呢,谢谢大家了。...
  7. python 服务端渲染_客户端渲染和服务器渲染的区别
  8. TensorFlow 资源汇集
  9. acs880变频器选型手册_据说这是变频器知识里讲的最全的了,小编拿来分享给大家!...
  10. 【Arduino】HX711驱动程序