Exploring Virtual Function

| | 评论(0)

Microsoft define "virtual function" as the following words in MSDN:

A virtual function is a member function that you expect to be redefined in derived classes. When you refer to a derived class object using a pointer or a reference to the base class, you can call a virtual function for that object and execute the derived class's version of the function. 虚函数是指一个类中你希望重定义的成员函数,当你用一个父类指针或者引用访问一个子类对象的时候,你调用的虚函数,实际调用的是继承类的版本。

这话不是很明白。Let me write a example in VC6.0.

/** @FILE testv.cpp
@Description to test how virtual function is implemented
@Author Mach
@Date 2004.07.05
*/

#include <iostream>
#include <conio.h>
using namespace std;

class Parent
{
public:
void FunctionA();
virtual void FunctionB();
};

void Parent::FunctionA()
{
cout<< "In Parent, FunctionA()\n";
}

void Parent::FunctionB()
{
cout<< "In Parent, FunctionB()\n";
}

class Child : public Parent
{
void FunctionA();
//virtual关键字只需要在parent类里申明就可以了,这里FunctionB继承了virtual特性
void FunctionB();
};

void Child::FunctionA()
{
cout<< "In Child, FunctionA()\n";
}

void Child::FunctionB()
{
cout<< "In Child, FunctionB()\n";
}

void main()
{
Parent parent;
Child child;
Parent *p;

if( getchar()=='c')
p=&child;
else
p=&parent;
p->FunctionA();
p->FunctionB();
}

VC++6.0编译并运行,输入字母c,把child的地址赋给p,得到下面的结果:
In Parent, FunctionA()
In Child, FunctionB()

不输入c, 把parent的地址赋给p的结果是
In Parent, FunctionA()
In Parent, FunctionB()

很显然,在输入c的情况下,p->FunctionB()是一个聪明的组合,它能判别指针实际上(而不是定义上)指向的是什么类来调用该类的虚函数。正式的说法叫作虚函数的动态联编(dynamically linking)功能,即在运行期确定调用哪个类的虚函数。

打破砂锅问到底,这种动态联编是怎么实现的?查书的结果是vtable. 还是做一个例子来说明问题吧。

#include <iostream>
using namespace std;

class Parent
{
public:
unsigned int a;
void FunctionA();
virtual void FunctionB();
};

void main()
{
cout<< sizeof(Parent) << endl;
}

输出是8. 如果把FunctionB()前面的virtual去掉,输出是4, 这正好是unsigned int的长度。就是说,非虚函数是不占空间的,而虚函数则要占4个字节,这看起来正好是一个指针的长度啊。具体如何,还是回到本文的第一个例子,在VC里面调试,记得把Disassembly打开。断点设在p->FunctionA();前面,在前面输入'c'.

51: p=&child;
00401335 lea ecx,[ebp-8]
00401338 mov dword ptr [ebp-0Ch],ecx

52: else
0040133B jmp main+83h (00401343)
53: p=&parent;
0040133D lea edx,[ebp-4]
00401340 mov dword ptr [ebp-0Ch],edx

54: p->FunctionA();
00401343 mov ecx,dword ptr [ebp-0Ch]
00401346 call @ILT+75(Parent::FunctionA) (00401050)

55: p->FunctionB();
0040134B mov eax,dword ptr [ebp-0Ch] ;
把指针p移到EAX
0040134E mov edx,dword ptr [eax] ;把指针p指向的地址的内容移到EDX,也就是说,child对象的前四个字节copyEDX
00401350 mov esi,esp
00401352 mov ecx,dword ptr [ebp-0Ch]
00401355 call dword ptr [edx]
;哈!原来child对象的前四个字节就是一个函数指针啊
00401357 cmp esi,esp
00401359 call __chkesp (00409030)

56: }
0040135E pop edi
0040135F pop esi
00401360 pop ebx
00401361 add esp,50h
00401364 cmp ebp,esp
00401366 call __chkesp (00409030)
0040136B mov esp,ebp
0040136D pop ebp
0040136E ret

在上面地址00401355处用step into跟踪进去看看,原来vtable的真面目是这样的:

@ILT+95(?FunctionB@Child@@EAEXXZ):
->00401064 jmp Child::FunctionB (00401270)
@ILT+100(?eof@?$char_traits@D@std@@SAHXZ):
00401069 jmp std::char_traits<char>::eof (00401910)
@ILT+105(??1sentry@?$basic_ostream@DU?$char_traits@D@std@@@std@@QAE@XZ):
0040106E jmp std::basic_ostream<char,std::char_traits<char> >::sentry::~sentry (00401a10)
@ILT+110(?id@?$ctype@G@std@@$D):
00401073 jmp std::ctype<unsigned short>::id (00402100)
@ILT+115(?id@?$ctype@G@std@@$E):
00401078 jmp std::ctype<unsigned short>::id (004021a0)
@ILT+120(??0Child@@QAE@XZ):
0040107D jmp Child::Child (004013e0)

现在明白了。C++里所有的函数(不光是虚函数)都在vtable里面占了5个字节,里面写着一个跳转指令,以跳到真正的函数体。同时,虚函数在对象里面某个特定偏移存储指向这个跳转指令的指针。

最后,我觉得这个vtable的实现有点傻。其实,只要放函数体的地址就好了,多一个jmp, 就多占一个字节,而且四字节对齐原则也被破坏了。有情报说,GNU C++实现的vtable就是只有函数体地址的,而且虚函数的指向vtable项的指针是存在对象尾部。容我再探。

posted 2004.07.06 Tuesday

分类

发表评论


关于此日记

此日记由mach发表于July 6, 2004 12:17 PM

此Blog上的上一篇日记方老师的逸闻

此Blog上的下一篇日记Yet Another Giga Bytes Email

主索引归档页可以看到最新的日记和所有日记。

Powered by Movable Type 4.23-en