在 C++ 中,虛函數是實現多態的核心機制。本文將通過具體的代碼示例、內存佈局輸出以及匯編代碼分析,深入探討虛函數調用的底層實現細節,揭示靜態綁定與動態綁定的本質區別。
我們定義一個包含虛函數的類 C041:
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
執行以下代碼並打印對象詳情:
C041 obj;
PRINT_DETAIL(C041, obj)
PRINT_VTABLE_ITEM(obj, 0, 0)
輸出結果:
The detail of C041 is 14 b3 45 00 01
obj : objadr:0012F824 vpadr:0012F824 vtadr:0045B314 vtival(0):0041DF1E
可見對象起始地址存放虛表指針(vptr),指向虛表(vtable),虛表首項存放 C041::foo() 的函數地址(實為跳轉指令)。
代碼 obj.foo(); 生成的匯編:
004230DF lea ecx,[ebp+FFFFF948h]
004230E5 call 0041DF1E
與普通成員函數調用相同,call 直接使用確定地址,編譯時已決議。這說明通過對象調用虛函數屬於靜態綁定,不會產生多態行為。
01 004263F0 push ebp
02 004263F1 mov ebp,esp
...
13 00426410 mov dword ptr [ebp-8],ecx
14 00426413 mov eax,dword ptr [ebp-8]
15 00426416 mov byte ptr [eax+4],2
...
第 14、15 行顯示:this 指針存入 eax,成員變量 c_ 位於 [eax+4],跳過了對象頭部的 4 字節虛表指針。
代碼 C041 * pt = &obj; pt->foo(); 生成的匯編:
01 004230F6 mov eax,dword ptr [ebp+FFFFF900h]
02 004230FC mov edx,dword ptr [eax]
03 004230FE mov esi,esp
04 00423100 mov ecx,dword ptr [ebp+FFFFF900h]
05 00423106 call dword ptr [edx]
eax(同時也是 vptr 地址)。eax 指向的值(即虛表地址)存入 edx。ecx 傳遞 this 指針。call dword ptr [edx] 間接調用虛表首項函數地址。這就是動態綁定:編譯器不固定函數地址,而是通過對象的 vptr 找到其類型對應的虛表,再取具體函數地址。具體調用哪個函數取決於對象的實際類型,而非指針類型,這正是多態發生的原因。
類 C051 繼承自 C041 和 C042(均含虛函數),對象包含兩個 vptr。動態轉換時編譯器調整指針位置:
C051 obj;
C041 * pt1 = dynamic_cast<C041*>(&obj); // 無需調整
C042 * pt2 = dynamic_cast<C042*>(&obj); // 需調整指針至第二個 vptr
調用 pt1->foo() 和 pt2->foo2() 產生的匯編機制完全一致,僅指針基址不同。這證明了多重繼承下保留多個虛表是為了方便指針類型轉換時的位置調整,從而統一調用機制。