分享
 
 
 

深入研究虚函数和vtable

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

深入研究虚函数和vtable

国防科技大学计算机学院 褚瑞

在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念。因为它充分体现了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广。比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说,它们都是虚函数。难怪有人甚至称虚函数是C++语言的精髓。

那么,什么是虚函数呢,我们先来看看微软的解释:

虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本。

——摘自MSDN

这个定义说得不是很明白。MSDN中还给出了一个例子,但是它的例子也并不能很好的说明问题。我们自己编写这样一个例子:

#include "stdio.h"

#include "conio.h"

class Parent

{

public:

char data[20];

void Function1();

virtual void Function2(); // 这里声明Function2是虚函数

}parent;

void Parent::Function1()

{

printf("This is parent,function1\n");

}

void Parent::Function2()

{

printf("This is parent,function2\n");

}

class Child:public Parent

{

void Function1();

void Function2();

} child;

void Child::Function1()

{

printf("This is child,function1\n");

}

void Child::Function2()

{

printf("This is child,function2\n");

}

int main(int argc, char* argv[])

{

Parent *p; // 定义一个基类指针

if(_getch()=='c') // 如果输入一个小写字母c

p=&child; // 指向继承类对象

else

p=&parent; // 否则指向基类对象

p->Function1(); // 这里在编译时会直接给出Parent::Function1()的

入口地址。

p->Function2(); // 注意这里,执行的是哪一个Function2?

return 0;

}

用任意版本的Visual C++或Borland C++编译并运行,输入一个小写字母c,得到下面的结果:

This is parent,function1

This is child,function2

为什么会有第一行的结果呢?因为我们是用一个Parent类的指针调用函数Fuction1(),虽然实际上这个指针指向的是Child类的对象,但编译器无法知道这一事实(直到运行的时候,程序才可以根据用户的输入判断出指针指向的对象),它只能按照调用Parent类的函数来理解并编译,所以我们看到了第一行的结果。

那么第二行的结果又是怎么回事呢?我们注意到,Function2()函数在基类中被virtual关键字修饰,也就是说,它是一个虚函数。虚函数最关键的特点是“动态联编”,它可以在运行时判断指针指向的对象,并自动调用相应的函数。如果我们在运行上面的程序时任意输入一个非c的字符,结果如下:

This is parent,function1

This is parent,function2

请注意看第二行,它的结果出现了变化。程序中仅仅调用了一个Function2()函数,却可以根据用户的输入自动决定到底调用基类中的Function2还是继承类中的Function2,这就是虚函数的作用。我们知道,在MFC中,很多类都是需要你继承的,它们的成员函数很多都要重载,比如编写MFC应用程序最常用的CView::OnDraw(CDC*)函数,就必须重载使用。把它定义为虚函数(实际上,在MFC中OnDraw不仅是虚函数,还是纯虚函数),可以保证时刻调用的是用户自己编写的OnDraw。虚函数的重要用途在这里可见一斑。

在了解虚函数的基础之上,我们考虑这样的问题:一个基类指针必须知道它所指向的对象是基类还是继承类的示例,才能在调用虚函数时“自动”决定应该调用哪个版本,它是如何知道的?有些讲C++的书上提到,这种“动态联编”的机制是通过一个“vtable”实现的,vtable是什么?微软在关于COM的文档里这样描述:

vtable是指一张函数指针表,如同C++中类的实现一样,vtable中的指针指向一个对象支持的接口成员函数。

——摘自MSDN

很遗憾,微软这次还是没有把问题说清楚,当然,上面的文档本来就是关于COM的,与我们关心的问题不同。

那么vtable是什么?我们先来看看下面的实验:

在前面的示例程序中加一句 printf(“%d”,sizeof(Child)); 运行,然后去掉Function2()前的virtual关键字,再运行,得到这样的结果:当Function2定义成虚函数的时候,结果是24,否则结果是20。也就是说,如果Function2不是虚函数,一个Child类的示例所占空间的大小仅仅是它的成员变量data数组的大小,如果Function2是虚函数,结果多了4个字节。我们使用的是32位的Visual C++ 6.0,4个字节恰好是一个指针,或者是一个整数所占的空间。

那么这多出来的四个字节究竟起到了什么作用?

用Visual C++打开前面的示例程序,在main函数中p->Function1(); 一句前面按F9设断点,按F5开始调试,输入一个小写c,程序停到了我们设的断点上。找到Debug工具条,按Disassembly按钮,如图所示:

我们看到了反汇编后的代码。由上图可见,对Function1和Function2的调用反汇编后生成的代码截然不同。Function1不是虚函数,因此对它的调用仅仅被编译成为一条call指令,转向Parent::Function1子程序;而Function2是虚函数,它的代码要复杂一些,我们来仔细分析:

45: p->Function2();

004012CA mov eax,dword ptr [ebp-4]

// eax就是我们的p指针

004012CD mov edx,dword ptr [eax]

// edx取child对象头部四个字节

004012CF mov esi,esp

004012D1 mov ecx,dword ptr [ebp-4]

// 可能要检查栈,不管它

004012D4 call dword ptr [edx]

// 注意这里,调用了child对象头部的一个函数指针

004012D6 cmp esi,esp

004012D8 call __chkesp (004013b0)

这里最关键的一句是call dword ptr[edx],edx是child对象头部,前面我们分析过了,child对象共有24字节,其中成员变量占用20字节,还有4个字节作用未知。现在从这段汇编代码上看,那4个字节很可能就是child对象开头的这个函数指针,因为编译器并不知道我们的成员变量data是做什么用的,更不可能把data的任何一部分当成一个函数指针来处理。

那么这个函数指针会跳转到那里去呢?我们按F10单步运行到这个call指令,然后按F11跟进去:

00401032 jmp Parent::Function2 (0040bfe0)

00401037 jmp Parent::Parent (004010d0)

→ 0040103C jmp Child::Function2 (00401250)

00401041 jmp Child::Child (004011c0)

光标停在了第三行,40103C的地方,执行这里的jmp指令后,又跳转到Child::Function2的位置,从而得到我们上面所看到的结果。

这并不是最终的结论,我们看看40103C周围的几行代码,连续几行全都是jmp指令,这是什么程序结构?有汇编语言编程经验的朋友可能会想起来了,这是一张入口表,分别存放着到几个重要函数的跳转指令!我们再回去看看微软对于vtable的描述:vtable是指一张函数指针表,(如同C++中类的实现一样,)vtable中的指针指向(一个对象支持的接口)成员函数。打括号的字不要看,这句话的主干就是:vtable是一张函数指针表,指向成员函数。种种事实证明,上面的四行代码就是我们要找的这个vtable!

现在我们应该对虚函数的原理有一个认识了。每个虚函数都在vtable中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应位置。调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。而不像普通函数那样简单地跳转到一个固定地址。

以上结论仅仅是针对Visual C++ 6.0编译器而言的,对于其他编译器,具体实现并不完全相同,但都大同小异。著名的“绿色兵团”杂志上撰文介绍,Linux平台上的GNU C++编译器就把指向vtable的指针放在对象尾部而不是头部,而且vtable中仅仅存放虚函数的入口地址,而不是跳转到虚函数的指令。具体的一些细节,篇幅所限,我们这里不再讨论,希望有兴趣的朋友能继续研究。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有