分享

c++虚函数机制

 酒一壶 2010-04-06
虚函数实现机制
2009-06-07 10:39

1、c++实现多态的方法

其实很多人都知道,虚函数在c++中的实现机制就是用虚表和虚指针,但是具体是怎样的呢?从more effecive c++其中一篇文章里面可以知道:是每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};

class B : public A
{
public:
    void g();
private:
    int b;
};

//A,B的实现省略

因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::f 的地址

A::g 的地址

B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::f 的地址
B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:

vptr : 指向B的虚表vtableB

int a: 继承A的成员

int b: B成员

当如下语句的时候:
A *pa = &bB;

pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)

那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。

这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)

另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。我以上的结果是根据g++ 4.3.4经过反汇编分析出来的。

2、两种多态实现机制及其优缺点

除了c++的这种多态的实现机制之外,还有另外一种实现机制,也是查表,不过是按名称查表,是smalltalk等语言的实现机制。这两种方法的优缺点如下:

(1)、按照绝对位置查表,这种方法由于编译阶段已经做好了索引和表项(如上面的call *(pa->vptr[1]) ),所以运行速度比较快;缺点是:当A的virtual成员比较多(比如1000个),而B重写的成员比较少(比如2个),这种时候,B的vtableB的剩下的998个表项都是放A中的virtual成员函数的指针,如果这个派生体系比较大的时候,就浪费了很多的空间。

比如:GUI库,以MFC库为例,MFC有很多类,都是一个继承体系;而且很多时候每个类只是1,2个成员函数需要在派生类重写,如果用C++的虚函数机制,每个类有一个虚表,每个表里面有大量的重复,就会造成空间利用率不高。于是MFC的消息映射机制不用虚函数,而用第二种方法来实现多态,那就是:

(2)、按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。(关于MFC的消息映射的实现,看下一篇文章)

3、总结:

如果继承体系的基类的virtual成员不多,而且在派生类要重写的部分占了其中的大多数时候,用C++的虚函数机制是比较好的;

但是如果继承体系的基类的virtual成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用名称查找表,这样效率会高一些,很多的GUI库都是这样的,比如MFC,QT

PS. 其实,自从计算机出现之后,时间和空间就成了永恒的主题,因为两者在98%的情况下都无法协调,此长彼消;这个就是计算机科学中的根本瓶颈之所在。软件科学和算法的发展,就看能不能突破这对时空权衡了。呵呵

何止计算机科学如此,整个宇宙又何尝不是如此呢?最基本的宇宙之谜,还是时间和空间~

C++实现多态靠两种方案,静态的重载和动态的虚函数机制.下面通过VC6环境分析下虚函数的实现机制.
(其实在VC6中,重载的实现方式是名字粉碎,即给函数通过某种方式起个别名,实现"多态",个人认为并不是真正的多态)
代码经VC6+WINXP SP2编译通过.
#include <iostream.h>
class CFather
{
public:
virtual void virFunc();{cout<<"CFather::virFunc()"<<endl;}
CFather();
virtual ~CFather(); //虚函数定义
};
class CSon1 : public CFather
{
public:
void virFunc();{cout<<"CSon1::virFunc()"<<endl;} //重写
CSon1();
virtual ~CSon1();
};
class CSon2 : public CFather
{
public:
void virFunc();{cout<< "CSon2::virFunc()" <<endl;} //重写
CSon2();
virtual ~CSon2();

};
在父类中定义了虚函数virFunc,两个子类分别重写,默认也为virtual类型.main 函数中调用如下:
int main(int argc, char* argv[])
{
CFather father;
CSon1 son1;
CSon2 son2;

CFather* pObj = &father; //父类指针

pObj->virFunc();

pObj = &son1; //子类指针

pObj->virFunc();

pObj = &son2; //子类指针

pObj->virFunc();

return 0;
}

根据虚函数机制,结果应该为:
CFather::virFunc()
CSon1::virFunc()
CSon2::virFunc()

这里解释下,在C++中,基类指针可以指向其派生类,是实现动态多态的必要前提.

那么,在VC6中是如何实现虚函数呢?答案是虚函数表。看下编译时的内存情况。
当执行至CFather father 后,观察。(不同机器地址可能有所不同)
&father 的地址为:0x0012ff70,打开memory,该地址内容为:
0012FF70 1C 80 42 00 B0 FF 12 00 3B 6A 41 00 00 00 00 00
首地址的4字节内容为:0042801c,这就是虚表的入口,查看memory,该地址内容为:
0042801C 46 10 40 00 64 10 40 00 00 00 00 00 43 46 61 74
由于虚表中存放的都是函数指针,所以,每4个字节为一个地址,可以从这个表中找到两个地址:0x00401046,0x00401064。分别对应virFunc和析构函数。

同理,看一下son2的情况,换个观察方式。
当执行至 CSon2时,变量监视窗口可以看到son2的__vfptr的值0x00428086,其下分别为__vfptr[0],__vfptr[1],值分别为0x00401014,0x00401023。有点眼熟,对,这就是son2类的虚表,只是在从CFather继承来的时候,重写的函数地址被覆盖了。

以上为编译时候内存的情况,通过分析我们可以得知,在实例化类的同时,实例的首地址存储其虚表的地址。当子类重写时,将其重写的地址覆盖从父类继承来的虚表。当不同指针调用时,根据其虚表选取不同的函数,实现多态性.也就是说,这样的调用是在运行时才绑定的,所以称为动态的多态性.

然后我们看下反汇编的情况。

编译时单步进入00401690 call @ILT+55(CFather::CFather) (0040103c)

004010E9 pop ecx
004010EA mov dword ptr [ebp-4],ecx
004010ED mov eax,dword ptr [ebp-4]
004010F0 mov dword ptr [eax],offset CFather::`vftable' (0042801c)

我们来分析这四行代码。
首先,pop ecx,
mov dword ptr [ebp-4],ecx
将ecx出栈,并将其值按两个字(4个字节)传给ebp-4中的地址。通过观察registers(寄存器窗口),ecx=0012ff70。这里
的ecx其实就是实例对象的this指针。
然后,mov eax,dword ptr [ebp-4]
将this指针传值给寄存器eax。因为汇编中不允许两个内存地址的操作,所以必须

采用eax做中转.
最后,mov dword ptr [eax],offset CFather::`vftable' (0042801c)
使用offset,将虚表的地址按两个字传给eax中的地址,即this指针指向了虚表地址。这就可以解释为什
么对象的首地址都是虚表的地址了。

子类实现方式同上,只是在call本身的构造之前,首先要进行父类的构造。

另外,在编译的时候,根据函数定义的先后顺序,在虚拟表中安排函数的位置,其实就是定义了一个函数指针的数组。当采用相应的对象调用函数时,在数组中相应的偏移寻找函数。

还要注意,一般在类定义的时候,构造函数不可以采用虚函数,而析构函数则最好采用虚拟函数

有关虚拟函数的定义,概念等以及汇编知识等请参考相应资料。本文不再讨论。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多