c++ 虚函数表

先看以下单继承类层次:

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++ 之类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值