先看以下单继承类层次:
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 ( |
4 |
m_data1 |
4 |
a2:
vfptr ( |
4 |
m_data1 |
4 |
m_data2 |
4 |
a3:
vfptr ( |
4 |
m_data1 |
4 |
m_data2 |
4 |
m_data3 |
4 |
虚函数表结构
vftable1:
函数 |
4 |
函数 |
4 |
vftable2:
函数 |
4 |
函数 |
4 |
函数 |
4 |
vftable3:
函数 |
4 |
函数 |
4 |
函数 |
4 |
函数 |
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 ( |
4 |
m_data1 |
4 |
vfptr ( |
4 |
m_data2 |
4 |
vfptr ( |
4 |
m_data3 |
4 |
m_dataC |
4 |
虚函数表内存结构
::
vftableC1
函数 |
4 |
一份代码经过偏移修正的新的 |
4 |
一份代码经过偏移修正的新的 |
4 |
一份代码经过偏移修正的新的 |
4 |
函数 |
4 |
vftableC2
一份代码经过偏移修正的新的 |
4 |
函数 |
4 |
vftableC3
一个代码经过偏移修正的新的 |
4 |
函数 |
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++
之类。