本文分析虛函式的小秘密,通過幾個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多個實現。
注:虛函式表共享在裡沒有被提及,但是並不影響分析。
所有的這些結果,根據經驗推算而來,不代表實現中一定是這樣。
GRUB的小秘密
我看了一下grub的源 主要的竅門是這樣的 grub分兩個部分stage1,stage2 stage1.5 stage1就是512的引導區了,它載入放在檔案系統中的stage2。而stage2完成grub的其它的事情,命令列,配罝,從檔案系統中載入kernel。但是stage1只能有512個位元組的...
background的小秘密
background image url 預設拉伸鋪滿background size 100px 100px 背景大小有兩個引數長和寬background repeat no repeat 預設repeat,還有repeat x和repeat y常用為no repeatbackground posi...
int float double 的那點小秘密
偶編c的時候,有的時候就是因為這些float int double之間的混算錯得一塌糊塗。這位仁兄說得挺好的,不過要想在深入了解下去,可就麻煩了。如果沒那知識,就一定要注意強制型別轉換後賦值和運算,比較保險!原文如下 抱歉我用了乙個這麼 二 的題目,不過二點就二點吧,希望內容還不算太二。其實學習過程...