理解虛方法

Andy Niu發表於2013-12-18

1、什麼是虛方法?

  考慮Animal* pa = new Dog(); pa表面型別是Animal,實際型別是Dog。可以理解為,pa說,我指向Animal,說法是對的。但是不具體,實際上,pa指向Dog。pa->Say()是虛方法,在編譯期,編譯器只知道pa 的表面型別,不知道該呼叫Animal 的Say方法還是Dog 的Say方法,所以才叫做“虛方法”。只有在執行期,才根據pa 的真實型別,確定呼叫哪個方法。這就是虛方法。

2、為什麼需要虛方法?它解決了什麼問題?

  簡單說,就是為了面向介面程式設計,庫的提供者暴露介面,隱藏實現。庫的使用者不需要知道內部的實現細節。

3、它是如何解決的?

  虛方法也就是執行時多型。要實現執行時多型,怎麼辦?思考,執行時多型的本質特徵是,相同的方法名,卻導致呼叫不同的方法。因此,這裡需要加一個間接層,內部封裝,暴露介面。現在簡單講一下,目前的C++是如何實現多型的?

  考慮,Animal類有一個虛方法表(可認為是一個陣列,元素是方法指標),裡面有100個虛方法,Dog類會整體拷貝Animal類的虛方法表,對於Dog類重寫的方法,在虛方法表的對應位置偷樑換柱,換上重寫後的方法,對於Dog類新增的虛方法,在虛方法表中後面加上。

  類的物件記憶體中只有,例項欄位和vptr兩塊內容,其中vptr指向類的虛方法表。假設Animal物件的記憶體佈局為:age,name,vptr,Animal物件的vptr指向Animal類的虛方法表。Dog物件的記憶體佈局為age,name,vptr,color,Dog物件的vptr指向Dog類的虛方法表。

  考慮pa->Say()虛方法的呼叫,編譯器知道Say是虛方法,通過vptr間接呼叫,在執行期才能確定下來。pa實際指向Dog物件,編譯器把pa指向內容當作Animal物件來解釋,這有沒有問題呢?

  在上面物件的記憶體佈局看到,Animal物件和Dog物件在前面部分是一樣的,Dog物件追加了一些記憶體。把pa指向的內容當作Animal物件解釋,取第三個欄位vptr,也就是Dog 物件的vptr,我們知道Do物件的vptr,指向Dog類的虛方法表。也就是說,會呼叫Dog類重寫的虛方法。

4、用法

  在父類中,為了表明是虛方法,在方法前加上virtual,子類重寫續方法。子類中的方法不需要在說明,是virtual,會自動生成為virtual,但是,為了直觀,建議子類中也使用virtual。

5、注意事項

  a、沒有虛方法的類,這個類也沒有對應的虛方法表,也可能是對應的虛方法表為空,類的物件也就沒有vptr。因此,不要隨便新增無用的虛方法,否則,會導致物件變大,要多一個vptr欄位。

  b、只有表面型別和真實型別不同的情況下,才存在多型。也就是說,只有指標或者引用才存在多型。為啥?對於類物件而言,真實型別,就是宣告的表面型別。子類物件賦值給父類物件,會出現物件切割,也就是把子類的部分切割掉,父類的部分整體拷貝。

  c、構造方法和析構方法中,不要呼叫虛方法,因為達不到預期的效果。為啥?子類構造方法是在父類構造方法完成的基礎上進行的。從這個角度講,構造析構可以類比穿衣脫衣。穿衣時,先穿內衣,再穿外套。脫衣時,先脫外套,再脫內衣。

考慮,在父類構造方法內呼叫虛方法,期望呼叫子類重寫的方法。這個是不行的,為啥?在父類構造方法中,當前物件不是一個完整的子類物件,還沒有子類部分,也就是說,當前情況下的真實型別就是父類物件,當然不可能有多型效果。

那麼析構方法呢?在父類的析構方法中呼叫虛方法,期望呼叫子類重寫的虛方法。這個也是不行的,為啥?從穿衣脫衣的例子中,我們知道在父類的析構方法中,子類部分已經銷燬了。

6、C++虛方法實現的侷限性

  通過上面的分析,知道C++虛方法的實現,是子類和父類都儲存一個虛方法表,這就導致同樣的值,儲存多次,耗費多餘的記憶體,特別是極端的情況下,頂層父類有許多虛方法,下面有衍生出一大堆的子類,每一個類都有一個很大的虛方法表。同樣的值,儲存多次是很愚蠢的。

  C++虛方法實現的優點也是很明顯的,由於子類和父類的虛方法表,在位置上一一對應,要麼是同樣的方法指標,要麼二者是重寫關係,當然也有可能是子類新增加了一些虛方法,對應的父類位置為空。這樣,就使得方法的查詢效率很高,根據下標直接定位。

相關文章