C對象布局及多態(tài)之虛成員函數(shù)調用

字號:

在構造函數(shù)中調用虛成員函數(shù),雖然這是個不很常用的技術,但研究一下可以加深對虛函數(shù)機制及對象構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。
    struct C180
    {
    C180() {
    foo();
    this->foo();
    }
    virtual foo() {
    cout << "<< C180.foo this: " << this << " vtadr: " << *(void**)this << endl;
    }
    };
    struct C190 : public C180
    {
    C190() {}
    virtual foo() {
    cout << "<< C190.foo this: " << this << " vtadr: " << *(void**)this << endl;
    }
    };
    父類中有一個虛函數(shù),并且父類在它的構造函數(shù)中調用了這個虛函數(shù),調用時它采用了兩種方法一種是直接調用,一種是通過this指針調用。同時子類又重寫了這個虛函數(shù)。
    我們可以來預測一下如果構造一個C190的對象會發(fā)生什么情況。
    我們知道,在構造一個對象時,首先會按對象的大小得到一塊內存(在heap上或在stack上),然后會把指向這塊內存的指針做為this指針來調用類的構造函數(shù),對這塊內存進行初始化。如果對象有父類就會先調用父類的構造函數(shù)(并依次遞歸),如果有多個父類(多重繼承)會依次對父類的構造函數(shù)進行調用,并會適當?shù)恼{整this指針的位置。在調用完所有的父類的構造函數(shù)后,再執(zhí)行自己的代碼。
    照上面的分析構造C190時也會調用C180的構造函數(shù),這時在C180構造函數(shù)中的第一個foo調用為靜態(tài)綁定,會調用到C180::foo()函數(shù)。第二個foo調用是通過指針調用的,這時多態(tài)行為會發(fā)生,應該調用的是C190::foo()函數(shù)。
    執(zhí)行如下代碼:
    C190 obj;
    obj.foo();
    結果為:
    << C180.foo this: 0012F7A4 vtadr: 0045C404
    << C180.foo this: 0012F7A4 vtadr: 0045C404
    << C190.foo this: 0012F7A4 vtadr: 0045C400
    和我們的分析大相徑庭。前2行是構造C190時的輸出,后1行是我們用靜態(tài)綁定方式調用的C190::foo()函數(shù)。第2行的輸出說明多態(tài)行為并沒有象預期的那樣發(fā)生。而且比較輸出的最后一列,發(fā)現(xiàn)在調用C180的構造函數(shù)時對象對應的虛表和構造后對象對應的虛表不是同一個。其實這正是奧秘的所在。
    為此我查了一下C 標準規(guī)范。在12.7.3條中有明確的規(guī)定。這是一種特例,在這種情況下,即在構造子類時調用父類的構造函數(shù),而父類的構造函數(shù)中又調用了虛成員函數(shù),這個虛成員函數(shù)即使被子類重寫,也不允許發(fā)生多態(tài)的行為。即,這時必須要調用父類的虛函數(shù),而不子類重寫后的虛函數(shù)。
    我想這樣做的原因是因為在調用父類的構造函數(shù)時,對象中屬于子類部分的成員變量是肯定還沒有初始化的,因為子類構造函數(shù)中的代碼還沒有被執(zhí)行。如果這時允許多態(tài)的行為,即通過父類的構造函數(shù)調用到了子類的虛函數(shù),而這個虛函數(shù)要訪問屬于子類的數(shù)據(jù)成員時就有可能出錯。
    我們看看VC7.1生成的匯編代碼就可以很容易的理解這個行為了。
    這是C190的構造函數(shù):
    01 00426FE0 push ebp
    02 00426FE1 mov ebp,esp
    03 00426FE3 sub esp,0CCh
    04 00426FE9 push ebx
    05 00426FEA push esi
    06 00426FEB push edi
    07 00426FEC push ecx
    08 00426FED lea edi,[ebp FFFFFF34h]
    09 00426FF3 mov ecx,33h
    10 00426FF8 mov eax,0CCCCCCCCh
    11 00426FFD rep stos dword ptr [edi]
    12 00426FFF pop ecx
    13 00427000 mov dword ptr [ebp-8],ecx
    14 00427003 mov ecx,dword ptr [ebp-8]
    15 00427006 call 0041D451
    16 0042700B mov eax,dword ptr [ebp-8]
    17 0042700E mov dword ptr [eax],45C400h
    18 00427014 mov eax,dword ptr [ebp-8]
    19 00427017 pop edi
    20 00427018 pop esi
    21 00427019 pop ebx
    22 0042701A add esp,0CCh
    23 00427020 cmp ebp,esp
    24 00427022 call 0041DDF2
    25 00427027 mov esp,ebp
    26 00427029 pop ebp
    27 0042702A ret