浅析C++虚函数表

虚函数表(Virtual Table)是指在每个包含虚函数的类中都存在着一个函数地址的数组.
talk is cheap, show me figure and code.

下面开始讲解。内容基于这个的:https://jocent.me/2017/08/07/virtual-table.html,不过那上面的程序有问题,在64位系统上运行会出错, 我对程序进行了修改和补充。

概述

在c++中,虚函数表是实现多态的关键,那么,相关问题来了:

虚函数表在哪里?怎么找到它的地址?它里面存了些什么玩意? 下面解答这些问题。

最简单的情况

首先回答自己的问题:虚函数表的地址存在于类实例的首部(如果出现了多继承,即一个孩子继承多个爸爸遗产的情况,有多个虚函数表, 每个虚函数表的地址存在于该基类的地址处。如果再考虑到数据对齐和padding, 需要一定的技巧找到non-first的虚函数表,这种情况比较复杂, 详细看我的代码:多继承 , )

虚函数表,如名字所述,里面存的是类的虚函数的地址。找到表的基地址后,可以不经过类的实例,强行调用虚函数^_^,后面的代码会有这种操作。

以下讨论基于64bit系统
假如有如下定义Base b; 那么虚函数表的地址vtptr的值就是:(long*)*(long*)&b,第一个虚函数vfunc1的地址就是:*(long*)*(long*)&b, vfunc2的地址是:*( (long*)*(long*)&b + 1 )。内存布局如图所示。

代码验证

多说一句,同一个类实例化多个对象,不同对象的虚函数表地址是一样的,这些对象的虚函数表在同一个地方放着。 输出为:

sizeof(b)=16
vtptr=0x55ce60bacd90
In Base vfunc1()
In Base vfunc2()
In Base vfunc3()
m_iMem1: 3
m_iMem2: 4
同一个基类实例化两个对象, 虚函数表的地址是一样的吗?----------------------------------------
v2tptr=0x55ce60bacd90

单继承,派生类未覆盖基类虚函数

如果派生类继承自基类,且只有一个基类,派生类没有覆盖基类的虚函数,虚函数表中先放基类虚函数(他老爸的虚函数)的地址,放完后再放派生类特有的虚函数(他自己的虚函数)的地址。 单继承时,内存布局如图所示:

单继承,派生类未覆盖基类虚函数测试代码

再多说一句,父类和子类的虚函数在不同的地方放着。 输出:

sizeof(d)=24
vtptr=0x557293cfdd48|addr of first derived object
In Base vfunc1()
In Base vfunc2()
In Base vfunc3()
In Devired vdfunc1().virtual void vdfunc1()
m_iMem1: 1
m_iMem2: 2

同一个子类实例化两个对象, 虚函数表的地址是一样的吗?----------------------------------------
v2tptr=0x557293cfdd48|addr of 2nd derived object
父类和子类的, 虚函数表的地址是一样的吗?----------------------------------------
bvtVtptr=0x557293cfdd78|addr of base object


单继承,派生类覆盖基类虚函数

如果派生类继承自基类,且只有一个基类,派生类覆盖基类的虚函数(派生类重写了基函数的函数体), 仍然先按照基类中虚函数声明的顺序放相应函数名的函数地址,只不过被覆盖的函数的地址变成派生类自己的函数地址,没有被覆盖的表项放的仍然是基类的虚函数地址。 内存布局如图所示:

代码: 单继承,派生类覆盖基类虚函数 输出:

sizeof(d)=24
deriveVtptr=0x55f57a26cd48
In Base vfunc1()
In Derived vfunc2()|overwride base
In Base vfunc3()
In Devired vdfunc1()|virtual void vdfunc1()
m_iMem1: 1
m_iMem2: 2

父类和子类的, 虚函数表的地址是一样的吗?----------------------------------------
bvtVtptr=0x55f57a26cd78|addr of base object

覆盖也正是实现多态的关键, 假设有:

Base *bp;
Base & bref;
Base b;
Derived d;
如果有:
bp = &d;
bref = d;
则bp->vfunc2()和bref.vfunc2()调用的是Derived中的成员函数, 如果有:
bp = &b;
bref = b;
则bp->vfunc2()和bref.vfunc2()调用的是Base中的成员函数

多继承下, 无虚函数覆盖

多继承时,类的内存布局为:按照定义派生类时指明基类的顺序依次放各个基类的虚函数表的地址和其他普通成员,最后放派生类自己的成员。 问题来了,这个类有多个虚函数表, 派生类自己定义的虚函数地址放在哪个表中呢?答案是:第一个基类的虚函数表的最后。 内存布局如图所示.

注意一些获取虚函数表地址的trick,由于C/C++中要考虑data-alignment和padding,这种做法是错误的:把某个基类前面的基类所有成员大小通过sizeof()运算符求该基类 虚函数表地址存放的位置。

还是例子比较清楚:以上面的 Derived这个class为例, 它的内存布局图,其中 vtptr1是一个指针,指向虚函数表的地址。 在64 bit 系统中,任何类型的指针大小为8 Bytes。m_iMem1和m_iMem2是int型数据,各占4 Bytes, vtptr2 同样占8 Bytes, m_iBase2Mem占4Bytes, vtptr3 同样占8 Bytes, m_iBase3Mem占4Bytes. 问题来了: m_iBase3Mem在类的内存中的相对类的基地址的移量是多少Bytes?

如果你认为是8+4+4 + 8+4 + 8 = 36, 恭喜你回答错误^_^.

为什么呢?第1个8+4+4是没有毛病的,8+4+4刚好符合对齐原则,不需要padding,第2个8+4 是需要对齐的,对齐的原则是把它padding到该类成员中最大的那个家伙的整数倍,Base2中最大的成员是8Bytes,所以 距离8+4最近而且是8的整数倍的数是:16,因此需要4个字节的padding,说白了就是 填成0.

因此正确答案是8+4+4 + 8+4+4(这个4是为了对齐出现的padding) + 8 = 40.

这也正是下面代码中求 Base3 的虚函数表的地址时,用如下求法的原因:
vtptr = (long*)*(long*)( (char*)dAddress +sizeof(Base) +sizeof(Base2) ) ;
下面的求法容易出错,仅仅把各个成员的sizeof()的结果加起来作为偏移地址,没有考虑padding,很容易得到错误的结果:
vtptr = (long*)*(long*)( (char*)dAddress +sizeof(long*)+2*sizeof(int) +sizeof(long*)+sizeof(int) ) ;// 错误做法

不相信? talk is cheap, show me the code : 多继承下, 无虚函数覆盖
输出

relative address of d.m_iMem1=8
relative address of d.m_iMem2=12
relative address of d.m_iBase2Mem=24
relative address of d.m_iBase3Mem=40
relative address of d.m_iDeriveMem1=44
vtptr=0x55968f854c70
In vfunc1()
In vfunc2()
In vfunc3()
In Devired vdfunc1()
m_iMem1: 1 
m_iMem2: 21 

In Base2 vfunc1()
In Base2 vfunc2()
m_iBase2Mem=31
In Base3 vfunc1()
In Base3 vfunc2()
m_iBase3Mem=41
m_iDeriveMem1=71

再多说一些关于padding的东西

给Base加一个char型数据,给Base3加一个char型数据,padding的情况要考虑一下。下图是内存分布,ptr表示虚函数指针


代码

好了,总算讲完了,经常分享总结,自己可以记得更牢,对别人也有帮助。