图解C++虚函数

Posted 海枫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解C++虚函数相关的知识,希望对你有一定的参考价值。

介绍

早在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() 
;

上述代码中定义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() 
;

base对象和derive对象内存结构

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

多态的实现

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

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

在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() 
;

基类的对象内存结构

下图分别定义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();

是如何实现从基类到派生类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++虚函数原理跟你之前了解的应该是一致的,只是很难技术细节你没有想过而已,但不管理怎么样,我们一起学习吧。

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

以上是关于图解C++虚函数的主要内容,如果未能解决你的问题,请参考以下文章

深入理解C++ 虚函数表

探索c++虚函数表

c++ 虚函数和纯虚函数

C++性能榨汁机之虚函数的开销

c++中,虚函数能不能被继承

C++中的虚函数以及虚函数表