| 虚函数的内存结构(二) 一,Class的布局 想要理解虚拟函数的内存布局,需要对class的结构入手,先从struct看起: struct{ int x,y; }; 如上的结构体在内存中如何存放呢?可以借用《inside the c++ program model》的图形来看该内容: 从上图来看在内存中,x,y其实是被分配到一块连续的内存空间中,这块内 存空间的大小是x,y的大小之和(不同的变量类型的话,可能会产生内存对齐的问题,这里不考虑)。 当我们有如下一个class的话: class point { private: int x,y; publice: void first(){}; } 如下声明: point pt; 那么pt 在内存中的存储方式如何呢?事实上它和struct的格式一样。并没有因为它是类而需要更多的信息(如函数名等信息)。而我刚开始的理解不是这样,因为当时没有考虑明白如下的方式中: point pt; pt.first(); 是如何调用函数的。事实上和在C语言中调用函数没有太多区别。 二,关于虚函数 先来看一段简单的程序: class point { public: void virtual first(); }; void point::first() { return; } void main() { point pt; pt.first(); } 反汇编,摘出它的_main函数: _main: 00401060: 55 push ebp 00401061: 8B EC mov ebp,esp 00401063: 83 EC 44 sub esp,44h 00401066: 53 push ebx 00401067: 56 push esi 00401068: 57 push edi 00401069: 8D 7D BC lea edi,[ebp-44h] 0040106C: B9 11 00 00 00 mov ecx,11h 00401071: B8 CC CC CC CC mov eax,0CCCCCCCCh 00401076: F3 AB rep stos dword ptr [edi] 00401078: 8D 4D FC lea ecx,[ebp-4] 0040107B: E8 85 FF FF FF call @ILT+0(??0point@@QAE@XZ) 00401080: 8D 4D FC lea ecx,[ebp-4] 00401083: E8 87 FF FF FF call @ILT+10(?first@point@@UAEXXZ) 00401088: 5F pop edi 00401089: 5E pop esi 0040108A: 5B pop ebx 0040108B: 83 C4 44 add esp,44h 0040108E: 3B EC cmp ebp,esp 00401090: E8 5B 00 00 00 call __chkesp 00401095: 8B E5 mov esp,ebp 00401097: 5D pop ebp 00401098: C3 ret 00401099: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌ 004010A9: CC CC CC CC CC CC CC 我们可以看到对于first的调用,和调用普通成员函数并没有太多区别,也就是说虚拟函数也和普通函数一样的方式被组织在代码段中,通过函数名直接调用。 但我们知道,虚拟函数的最大特点,是通过父函数的指针可以调用子函数的方法来实现多态。是如何实现的呢?通过父指针要确定子类的的类型,由于可能有多个子类,而且是运行时刻识别,这样就需要子类的对象本身包含自身的信息。方法其实就在class对象的存储内容上。 再看一个类: class point { Private: Int x,y; Public: Virtual void first(){}; Virtual void test(){}; } 当我们声明 point pt;的时候,内存中的存放形式如下: void first(); void two(); type info |
从图上可以看到,在pt的结构体内多了一个指针,该指针指向了该类所有的虚拟函数列表。该列表是各个虚拟函数所对应的函数地址。这样在动态调用的时候,就可以通过对象自身,而不是类来确定调用的函数。 再看一个例子: class point { public: void virtual first(); }; void point::first() { return; } void main() { point *pt = new point(); pt->first(); } 现在改用指针来调用该方法,反汇编该代码,摘取_main函数如下: _main: 00401060: 55 push ebp 00401061: 8B EC mov ebp,esp 00401063: 6A FF push 0FFh 00401065: 68 5B 08 41 00 push 41085Bh 0040106A: 64 A1 00 00 00 00 mov eax,fs:[00000000] 00401070: 50 push eax 00401071: 64 89 25 00 00 00 mov dword ptr fs:[0],esp 00 00401078: 83 EC 50 sub esp,50h 0040107B: 53 push ebx 0040107C: 56 push esi 0040107D: 57 push edi 0040107E: 8D 7D A4 lea edi,[ebp-5Ch] 00401081: B9 14 00 00 00 mov ecx,14h 00401086: B8 CC CC CC CC mov eax,0CCCCCCCCh 0040108B: F3 AB rep stos dword ptr [edi] 0040108D: 6A 04 push 4 0040108F: E8 6C 01 00 00 call ??2@YAPAXI@Z 00401094: 83 C4 04 add esp,4 00401097: 89 45 E8 mov dword ptr [ebp-18h],eax 0040109A: C7 45 FC 00 00 00 mov dword ptr [ebp-4],0 00 004010A1: 83 7D E8 00 cmp dword ptr [ebp-18h],0 004010A5: 74 0D je 004010B4 004010A7: 8B 4D E8 mov ecx,dword ptr [ebp-18h] 004010AA: E8 56 FF FF FF call @ILT+0(??0point@@QAE@XZ) 004010AF: 89 45 E4 mov dword ptr [ebp-1Ch],eax 004010B2: EB 07 jmp 004010BB 004010B4: C7 45 E4 00 00 00 mov dword ptr [ebp-1Ch],0 00 004010BB: 8B 45 E4 mov eax,dword ptr [ebp-1Ch] 004010BE: 89 45 EC mov dword ptr [ebp-14h],eax 004010C1: C7 45 FC FF FF FF mov dword ptr [ebp-4],0FFFFFFFFh FF 004010C8: 8B 4D EC mov ecx,dword ptr [ebp-14h] 004010CB: 89 4D F0 mov dword ptr [ebp-10h],ecx 004010CE: 8B 55 F0 mov edx,dword ptr [ebp-10h] 004010D1: 8B 02 mov eax,dword ptr [edx] 004010D3: 8B F4 mov esi,esp 004010D5: 8B 4D F0 mov ecx,dword ptr [ebp-10h] 004010D8: FF 10 call dword ptr [eax] 004010DA: 3B F4 cmp esi,esp 004010DC: E8 0F 06 00 00 call __chkesp 004010E1: 8B 4D F4 mov ecx,dword ptr [ebp-0Ch] 004010E4: 64 89 0D 00 00 00 mov dword ptr fs:[0],ecx 00 004010EB: 5F pop edi 004010EC: 5E pop esi 004010ED: 5B pop ebx 004010EE: 83 C4 5C add esp,5Ch 004010F1: 3B EC cmp ebp,esp 004010F3: E8 F8 05 00 00 call __chkesp 004010F8: 8B E5 mov esp,ebp 004010FA: 5D pop ebp 004010FB: C3 ret 这段代码中已经找不到含有first形式的函数调用,新的调用方式是: 004010D8: FF 10 call dword ptr [eax],通过ptr来调用函数。关于ptr的内容有些繁琐,一些细节我还是不太清楚。这边我所知道的就是通过它可以得到虚函数所存在的地址。 这样当用父指针指向子类对象的时候,由于子类本身带有了虚函数的信息,通过父类就可以直接调用到子类的虚函数,而不是父类自己的虚函数。当不是虚函数的时候,父类是就只能调用自己的函数了。 写到这儿,发现自己对很多概念还是不清晰,如this指针的用法,多重继承下的虚函数等。这篇文章写不清晰,同时发现自己的语文水平也有限!路漫漫其修远,不知道什么时候求索是个头啊! |