構造函式呼叫虛函式

2021-09-30 08:52:51 字數 4728 閱讀 3118

參考stroustrup的回答吧(

在建構函式中呼叫虛成員函式,雖然這是個不很常用的技術,但研究一下可以加深對虛函式機制及物件構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。

struct c180

virtual foo()

};struct c190 : public c180

virtual foo()

}; 父類中有乙個虛函式,並且父類在它的建構函式中呼叫了這個虛函式,呼叫時它採用了兩種方法一種是直接呼叫,一種是通過this指標呼叫。同時子類又重寫了這個虛函式。

我們可以來**一下如果構造乙個c190的物件會發生什麼情況。

我們知道,在構造乙個物件時,過程是這樣的:

1) 首先會按物件的大小得到一塊記憶體(在heap上或在stack上),

2) 把指向這塊記憶體的指標做為this指標來呼叫類的建構函式,對這塊記憶體進行初始化。

3) 如果物件有父類就會先呼叫父類的建構函式(並依次遞迴),如果有多個父類(多重繼承)會依次對父類的建構函式進行呼叫,並會適當的調整this指標的位置。在呼叫完所有的父類的建構函式後,再執行自己的**。

照上面的分析構造c190時也會呼叫c180的建構函式,這時在c180建構函式中的第乙個foo呼叫為靜態繫結,會呼叫到c180::foo()函式。第二個foo呼叫是通過指標呼叫的,這時多型行為會發生,應該呼叫的是c190::foo()函式。

執行如下**:

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行是我們用靜態繫結方式呼叫的c190::foo()函式。第2行的輸出說明多型行為並沒有象預期的那樣發生。而且比較輸出的最後一列,發現在呼叫c180的建構函式時物件對應的虛表和構造後物件對應的虛表不是同乙個。其實這正是奧秘的所在。

為此我查了一下c++標準規範。在12.7.3條中有明確的規定。這是一種特例,在這種情況下,即在構造子類時呼叫父類的建構函式,而父類的建構函式中又呼叫了虛成員函式,這個虛成員函式即使被子類重寫,也不允許發生多型的行為。即,這時必須要呼叫父類的虛函式,而不子類重寫後的虛函式。

我想這樣做的原因是因為在呼叫父類的建構函式時,物件中屬於子類部分的成員變數是肯定還沒有初始化的,因為子類建構函式中的**還沒有被執行。如果這時允許多型的行為,即通過父類的構造函式呼叫到了子類的虛函式,而這個虛函式要訪問屬於子類的資料成員時就有可能出錯。 

我們看看vc7.1生成的彙編**就可以很容易的理解這個行為了。 

這是c190的建構函式:

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 

開始部分的指令在前面幾篇中陸續解釋過,這裡不再詳述。我們看看第15是對父類的建構函式c180::c180()的呼叫,根據前文的說明,我們知道此時ecx中放的是this指標,也就是c190物件的位址。這時如果跳到this指標批向的位址看看會發現值為0xcccccccc即沒有初始化,虛表指標也沒有被初始化。那麼我們跟著跳到c180的建構函式看看。

01 00427040 push ebp 

02 00427041 mov ebp,esp

03 00427043 sub esp,0cch

04 00427049 push ebx 

05 0042704a push esi 

06 0042704b push edi 

07 0042704c push ecx 

08 0042704d lea edi,[ebp+ffffff34h]

09 00427053 mov ecx,33h

10 00427058 mov eax,0cccccccch

11 0042705d rep stos dword ptr [edi]

12 0042705f pop ecx 

13 00427060 mov dword ptr [ebp-8],ecx

14 00427063 mov eax,dword ptr [ebp-8]

15 00427066 mov dword ptr [eax],45c404h

16 0042706c mov ecx,dword ptr [ebp-8]

17 0042706f call 0041da8c

18 00427074 mov ecx,dword ptr [ebp-8]

19 00427077 call 0041da8c

20 0042707c mov eax,dword ptr [ebp-8]

21 0042707f pop edi 

22 00427080 pop esi 

23 00427081 pop ebx 

24 00427082 add esp,0cch

25 00427088 cmp ebp,esp

26 0042708a call 0041ddf2

27 0042708f mov esp,ebp

28 00427091 pop ebp 

29 00427092 ret 

看看第15行,在this指標的位置也就是物件的起始處,填入了乙個4位元組的值0x0045c404,其實這就是我們前面的列印過的c180的虛表位址。第16、17行和18、19行分別呼叫了兩次foo()函式,用的都是靜態繫結。這個就有點奇怪,因為對後乙個呼叫我們使用了this指標,照理應該是動態繫結才對。可這裡卻是靜態繫結,為什麼編譯器要做這個優化?我們繼承往後看。 

這個函式執行完後,我們再回到c190建構函式中,我們接著看c190建構函式彙編**的第17行,這裡又在物件的起始處重新填入了0x0045c400,覆蓋了原來的值,而這個值就是我們前面列印過的真正的c190的虛表位址。

也就是說vc7.1是通過在呼叫建構函式的真正**前把物件的虛指標值設定為指向對應類的虛表來實現c++規範的相應語義。c++標準中只規定了行為,並不規定具體編譯器在實現這一行為時所用的方法。象我們上面看到的,即使是通過this指標呼叫,編譯器也把它優化為靜態繫結,也就是說即使不做這個虛指標的調整也不會有錯。之所以要調整我想可能是防止在被呼叫的虛成員中又通過this指標來呼叫其他的虛函式,不過誰會這麼**呢? 

還有值得一提的是,vc7.1中有乙個擴充套件屬性可以用來抑制編譯器產生對虛指標進行調整的**。我們可以在c180類的宣告中加入這個屬性。

struct __declspec(novtable) c180

virtual foo()

}; 這樣再執行前面的**,輸出就會變成:

<< c180.foo this: 0012f7a4 vtadr: cccccccc

<< c180.foo this: 0012f7a4 vtadr: cccccccc

<< c190.foo this: 0012f7a4 vtadr: 0045c400 

由於編譯器抑制了對虛指標的調整所以在調c180的建構函式時虛指標的值沒有初始化,這時我們才看到多虧編譯器把第二個通過this指針對foo的呼叫優化成了靜態繫結,否則由於虛指標沒有初始化一定會出現乙個指標異常的錯誤,這就回答我們上面的那個問題。

在這種情況下產生的彙編**我就不列了,有興趣的朋友可以自己去看一看。另外對於析構函式的呼叫,也請有興趣的朋友自行分析一下。 

另外這個屬性在atl的**中大量的使用。在atl中介面一般為純虛基類,如果不用這個優化屬性,由於在子類即實現類的建構函式中要呼叫父類的建構函式,而編譯器產生的父類建構函式又要設定虛指標的值。所以編譯器必須要把父類的虛表構建出來。而實際上這個虛表是沒有任何意義的,因為atl的純虛介面類的虛函式都是無實現的。這樣不僅僅是多了幾行無用的設值指令,同時也浪費了空間。有興趣的朋友可以自行驗證一下。 

構造函式呼叫虛函式

在建構函式中呼叫虛成員函式,雖然這是個不很常用的技術,但研究一下可以加深對虛函式機制及物件構造過程的理解。這個問題也和一般直觀上的認識有所差異。先看看下面的兩個類定義。struct c180 virtual foo struct c190 public c180 virtual foo 父類中有乙個...

C 建構函式中呼叫虛函式

我們知道 c 中的多型使得可以根據物件的真實型別 動態型別 呼叫不同的虛函式。這種呼叫都是物件已經構建完成的情況。那如果在建構函式中呼叫虛函式,會怎麼樣呢?有這麼一段 class a virtual void func void test public int m ival class b publ...

C 建構函式中呼叫虛函式

談談關於建構函式中呼叫虛函式的情況,僅討論單繼承,不考慮虛擬繼承和多重繼承。測試平台 vs2013 win7x64 乙個例子 include include class base public virtual void func class deri public base public virtu...