Exploring Virtual Function
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 void Parent::FunctionA() void Parent::FunctionB() class Child : public Parent void Child::FunctionA() void Child::FunctionB() void main() if( getchar()=='c') |
用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 void main() |
输出是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对象的前四个字节copy到EDX中
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
分类
Technology

发表评论