1. 程式人生 > >虛函數的小秘密

虛函數的小秘密

++ 問題 adjust 理解 跳轉 data 沒有 支持 的確

本文分析虛函數的小秘密,通過幾個case說明為了支持虛函數,應該有什麽樣的約定,生成什麽樣的代碼。



C++中虛函數用於實現多態:即方法調用和對象的動態類型綁定。

詳細地說對A*類型指針p指向A的公有派生類B的對象,A中有虛函數foo,B中給定foo的還有一份實現,p->foo應該和B中的新實現綁定,而不是和A中的實現綁定。



一般而言。會在對象布局中插入一個虛函數表指針。在表中列出了全部的虛函數。

以下以這樣的模型為基礎討論。



先看基類A,假定有數據成員dataA,虛函數表指針vptrA。虛函數fooA()。

從fooA本身的實現來看,在thiscall的約定下,覺得ecx作為輸入參數,當中的值是this指針,this指針的類型當然是A*了。

要調用虛函數則要滿足例如以下條件:
1. 可以找到fooA的實現地址。
2. ecx中含有this,this的類型是A*(即this確實指向了A的對象,而不會是A的派生類的對象)。



在此,引入一個不變式:
假設有A* ptr;那麽ptr指向的內存數據的理解應該全然由A這個類型決定。不管ptr指向的確實是是一個A的對象,或者A的派生類的對象。

既然如此,派生類對象應該存在某段區域,這段區域能夠看到一個A的對象。ptr指向派生類時。應該指向派生類對象的這段區域。


[不變式使得向上向下轉型時有指針重設,使得在ptr->foo的代碼中能夠斷言ptr指向的對象是什麽,使得能夠用不變的幾步操作來實現對foo的調用]

case 1

假設我們有A* ptr = new A();虛函數的調用實現應該是,從ptr指向的數據拿到虛表指針,依據偏移,進而拿到A::fooA的詳細地址。將ptr直接放到ecx中。

所以滿足fooA的調用條件能夠滿足。


假設有B繼承自A,B中引入虛函數fooB,數據dataB。沒有override了fooA。一個可能的內存布局是:
vptrB dataB vptrA dataA。

case 2
假設有A* ptr = new B();依據前面的不變式ptr會指向vptrA的位置。假設在vptr中fooA的位置放上fooA實現的地址。這個時候調用方法沒問題,和前面討論的一樣。

case 3
假設有B* ptr = new B();依據類型信息B,通過偏移量拿到vptrA。進而能拿到fooA的地址。此外,拿到vptrA的同一時候,也拿到了A-sub-object的地址。將這個sub object的地址放到ecx中,於是虛函數調用條件滿足。

再考慮B中override了fooA的情況,最好還是設這個override的函數名為fooA_override_by_B:在構造B的時候。將fooA_override_by_B填入了vptrA中相應位置。

case 4
對於A* ptr = new B();傳入的ecx指向的是A這個sub-object,實際調用到的函數是fooA_override_by_B。而依據虛函數調用條件,fooA_override_by_B會覺得傳入的是B*。

所以fooA_override_by_B分為兩部分,當中一部分fooA_override_by_B_impl是詳細實現,會覺得傳入的this是B*的。還有一部分fooA_override_by_B_adjust會將傳入的A*調整為B*,然後跳轉到fooA_override_by_B_impl。

vptrA中放的應該是fooA_override_by_B_adjust。

case 5
對於B* ptr = new B();在外部將ecx指向了A-sub-object,在fooA_override_by_B_adjust中又將指向A-sub-object的對象調整為指向B的對象。

所以。在發生override時,一方面提供相應的實現函數,這個函數接受正在override的類的指針。還有一方面在被override的類的虛函數表中。放上調整函數,將父類指針調整為子類指針。這樣做是可行的,由於被override的類和正在override的類的信息是編譯時確定的。



討論到這裏,我們在有T* ptr時,調用虛函數的條件是這樣滿足的:
1. 依據T這個類型。及其繼承關系。考察被調用的虛函數,找到相應的虛表指針。然後依據虛函數在虛表中的位置。確定虛函數的地址。

當中T的類型,繼承關系,被調用的虛函數,某個類引入的虛函數在虛表中的位置,ptr的值,這5個量是已知的。未知的是虛函數的位置。


2. 相同被調用的虛函數所屬的類在T中的偏移也是個已知量,加上ptr就得到相應的sub-object的地址。



再看個多繼承的樣例,假設A,B作為基類。都有虛函數fooX。C繼承自A,B。override了fooX。

這個時候C中有三個虛函數表,在A的虛函數表中fooX應該是fooX_A_override_by_C_adjust。在B的虛函數表中foo應該是fooX_B_override_by_C_adjust。

和前面的不同,我們用fooX_A表示fooX是A中的,fooX_B是B中的,兩者名字同樣。我們用這個後綴以作差別。兩個adjust函數能夠分別將A和B的指針調整為C的指針。然後分別都跳轉到fooX_A_override_by_C_impl,fooX_B_override_by_C_impl實際上。這兩個impl函數是同一個。當然。第三個虛函數表是C自己能夠引入的虛函數,在此不影響討論。

虛函數有什麽秘密?
1.[指針約定、對象布局]形如T* ptr;應當覺得指向的內存開始sizeof(T)個字節確實是一個T的對象。

這就要求向上或向下轉型時。有指針重設。在對象布局中子類中存在某段區域。這段區域正好是基類對象。
2.[引入虛函數表]對象中有指針指向虛函數表。使得在指向不同的虛函數表的時候,虛函數調用有不同的表現。
3.[虛函數表中的項對this的需求]在call虛函數表的某一項時,ecx中保存的是虛函數表相應的對象的this指針。
4.[override後的this改動]在子類override時,對於每一處須要改動的虛函數表(一般僅僅有一個),因為3滿足,所以能夠插入將這個對象轉為子類對象的轉換代碼。然後跳轉到override後的實現。

5.[可能override多個虛函數表中的項]有多個須要改動的虛函數表時說明通過該對象同一時候override多個實現。


註:虛函數表共享在裏沒有被提及,可是並不影響分析。


全部的這些結果,依據經驗推算而來,不代表實現中一定是這樣。

虛函數的小秘密