先看以下单继承类层次:
class A1
{
public:
A1(){}
virtual fun(){}
virtual funA1(){}
long m_data1;
};
class A2:public A1
{
public:
A2(){}
virtual fun(){}
virtual funA2(){}
long m_data2;
};
class A3:public A2
{
public:
A3(){}
virtual fun(){}
virtual funA3(){}
long m_data3;
};
初始化以下变量
A1 a1;
A2 a2;
A3 a3;
此时 a1,a2,a3 在内存中的结构将如下图所示
a1:
vfptr ( 虚函数表指针,指向虚函数表 vftable1) | 4 字节 |
m_data1 | 4 字节 |
a2:
vfptr ( 虚函数表指针,指向虚函数表 vftable2) | 4 字节 |
m_data1 | 4 字节 |
m_data2 | 4 字节 |
a3:
vfptr ( 虚函数表指针,指向虚函数表 vftable3) | 4 字节 |
m_data1 | 4 字节 |
m_data2 | 4 字节 |
m_data3 | 4 字节 |
虚函数表结构
vftable1:
函数 A1:fun() 地址 | 4 字节 |
函数 A1:funA1() 地址 | 4 字节 |
vftable2:
函数 A2:fun() 地址 | 4 字节 |
函数 A1:funA1() 地址 | 4 字节 |
函数 A2:funA2() 地址 | 4 字节 |
vftable3:
函数 A3:fun() 地址 | 4 字节 |
函数 A1:funA1() 地址 | 4 字节 |
函数 A2:funA2() 地址 | 4 字节 |
函数 A3:funA3() 地址 | 4 字节 |
每个 C++ 类(基类或者 派生类 )在内存中对应着一个虚函数表( vftable ),无论有没有对该类初始化变量,这个虚函数表都始终存在。
从上图可以看出,一个类无论有多少个基类,其相应实例的虚函数表指针始终只有一个,并且严格指向这个类(而不是基类)的虚函数表。
每个派生类的 虚函数表 继承了它所有基类的 虚函数表 ,如果基类 虚函数表 中包含某一项,则其派生类的 虚函数表 中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类 虚函数表 的该项指向重载后的虚函数,没有重载的话,则沿用基类 虚函数表 的值。
再看以下代码
A3 a3;
A1 *pA1 = &a3;
A2 *pA2 = &a3;
A3 *pA3 = &a3;
不用说,你肯定知道 pA1,pA2,pA3 值一定相等。
接着我们看看以下多重继承类层次 :
class B1
{
public:
B1(){}
virtual fun(){}
virtual funB1(){}
long m_data1;
};
class B2
{
public:
B2(){}
virtual fun(){}
virtual funB2(){}
long m_data2;
};
class B3
{
public:
B3(){}
virtual fun(){}
virtual funB3(){}
long m_data3;
};
class C : public B1 , public B2, public B3
{
public:
C(){}
virtual fun(){}
virtual funC(){}
long m_dataC;
};
初始化变量:
C c;
此时变量 c 在内存中的结构将如下图所示:
vfptr ( 虚函数表指针,指向虚函数表 vftable_C1) | 4 字节 |
m_data1 | 4 字节 |
vfptr ( 虚函数表指针,指向虚函数表 vftable_C2) | 4 字节 |
m_data2 | 4 字节 |
vfptr ( 虚函数表指针,指向虚函数表 vftable_C3) | 4 字节 |
m_data3 | 4 字节 |
m_dataC | 4 字节 |
虚函数表内存结构 ::
vftableC1
函数 C:fun() 地址 | 4 字节 |
一份代码经过偏移修正的新的 B1:funB1() 函数地址 | 4 字节 |
一份代码经过偏移修正的新的 B2:funB2() 函数地址 | 4 字节 |
一份代码经过偏移修正的新的 B3:funB3() 函数地址 | 4 字节 |
函数 C:funC() 地址 | 4 字节 |
vftableC2
一份代码经过偏移修正的新的 C:fun() 函数地址 | 4 字节 |
函数 B2:funB2() 地址 | 4 字节 |
vftableC3
一个代码经过偏移修正的新的 C:fun() 函数地址 | 4 字节 |
函数 B3:funB3() 地址 | 4 字节 |
上面的偏移修正指的是先进入一小段代码, 修改This指针(ECX寄存器),
然后再跳转到原始的调用过程
接着看以下代码
C c;
B1* pB1 = &c;
B2* pB2 = &c;
B3* pB3 = &c;
C* pC = &c;
假设变量 C 的开始地址为 address
这时指针值依次为 pB1=address , pB2=address+4 , pB3=address+8 , pC=address
参照上面变量 C 的内存结构,你可能已经猜到为什么需要多个虚函数表了
想想当使用这些地址不相同的指针调用一个成员函数时会发生什么,例如 pC->funB2(),
假如 funB2() 需要访问对象内部数据 m_data2 ,这时按变量相对偏移计算地址时将出现错误。
关于 __declspec(novtable)
从上面例子你可以看到,如果一个基类只是作为抽象类(一般含纯虚函数),由于抽象类不能实例化对象,因此它的虚函数表将永远用不上,为节约内存空间,可以采用 __declspec(novtable) 指示编译器不为该抽象类生成虚函数表。
尾记:
大多数人可能没有必要钻这些细节,在大部分情况下,理解这篇文章对你可能有
些好处(比如ATL,那可是多重继承的聚集地),但可能于你的工作无多大帮助, 除非你正在打算写编译器或者设计 c++ 之类。