Virtual method table(虛擬方法表):java
一個虛擬方法表(VMT)/虛擬功能表/虛擬呼叫表/分發表是使用在編程語言中支持動態分發(或運行時方法綁定)的一種機制。編程
不管什麼時候,一個類定義了一個虛擬方法,大多數編譯器向類指定一個指向虛擬方法表(VMT或Vtable)的(虛擬)函數的指針數組添加一個隱藏的成員變量。這些指針在運行時用於適當的函數調用,由於在編譯時它可能還不知道是否要調用基函數或者是從繼承基類的類實現的派生類。數組
假設一個程序在繼承層次結構中包含幾個類:一個父類: Cat,兩個子類: HouseCat, Lion。Cat類定義了一個名叫speak的虛擬函數(方法),所以他的子類可能提供一個適當的實現。(例如:meow或者roar)。緩存
當程序調用一個Cat引用(能夠指代實例Cat,或者HouseCat,Lion實例)上調用speak函數時,這個代碼必須可以肯定調用應該被調用到的函數的實現。這取決於對象的實際類,而不是它聲明的類Cat。這個類一般不能靜態地(即,在編譯時)肯定類,因此編譯器也不能決定哪一個函數在運行時被調用了。這個調用必須動態地(即,在運行時)被分發到正確的函數。架構
實現這種動態分發有不少不一樣的方法,可是虛擬方法表解決方案在C++及其相關語言(如D和C#)中尤其常見。將對象的編程接口從實現分離出來的語言,像Visual Basic和 Delphi, 也可使用虛擬表實現。由於它容許對象使用不一樣的實現,只須要使用一組不一樣的方法指針。編程語言
內容ide
1. Implementation(實現)函數
2. Example(例子)佈局
3. Multiple inheritance and thunks(多重繼承和指針修復)性能
4. Invocation(調用)
5. Efficiency(效率)
6. Comparison with alternatives(比較和替代)
7. See also(參見)
8. Notes(筆記)
9. References(參考)
1. Implementation(實現)
一個對象的調度表將包含對象的動態綁定方法的地址,方法調用是經過從對象的調度表提取方法地址來執行的。調度表對於屬於同一類的全部對象是相同的,所以一般在它們之間共享。屬於類型兼容類的對象(例如繼承層次結構中的兄弟)將有相同佈局的調度表:給定方法的地址將對全部兼容類將以相同的偏量出現。所以,從給定的調度表偏移獲得方法的地址將得到對應於對象的實際類的方法[1]。
C++規範並無要求必須如何實現動態調度,可是編譯器在相同基礎模型上一般使用較小的變化。
典型地,編譯器爲每一個類建立一個單獨的虛擬表。當一個對象被建立時,一個指向這個虛擬表的指針,被稱爲虛擬表指針,vpointer或VPTR,做爲該對象的隱藏成員添加。所以,編譯器必須在每一個類的構造函數生成「隱藏」代碼,去初始化一個新對象的虛擬表指針到這個類的虛擬表指針。
不少編譯器將虛擬表指針做爲對象的最後一個成員,而另外的編譯器將虛擬指針表做爲對象的第一的成員。便攜式源代碼工做方式[2]。例如,g++之前將虛擬表指針放在對象的末尾[3]。
2. Example(例子)
思考下面C++語法的類聲明:
class B1 { public: void f0() {} virtual void f1() {} int int_in_b1; }; class B2 { public: virtual void f2() {} int int_in_b2; };
使用派生如下類:
class D : public B1, public B2 { public: void d() {} void f2() {} // override B2::f2() int int_in_d; };
下面是C++代碼片斷:
B2 *b2 = new B2(); D *d = new D();
g++ 3.4.6 從GCC爲對象b2產生下面32位存儲佈局[nb 1]:
b2:
+0: pointer to virtual method table of B2
+4: value of int_in_b2
virtual method table of B2:
+0: B2::f2()
下面是對象d的存儲佈局:
d:
+0: pointer to virtual method table of D (for B1)
+4: value of int_in_b1
+8: pointer to virtual method table of D (for B2)
+12: value of int_in_b2
+16: value of int_in_d
Total size: 20 Bytes.
virtual method table of D (for B1):
+0: B1::f1() // B1::f1() is not overridden
virtual method table of D (for B2):
+0: D::f2() // B2::f2() is overridden by D::f2()
注意這些函數在聲明(像f0()和d)時並不攜帶關鍵字virtual,通常不會出如今虛擬表中。由默認構造函數構成的是特例。
在類D中重寫方法f2()是經過複製虛擬方法表B2並將指針B2::f2()替換成D::f2()實現的。
3. Multiple inheritance and thunks(多重繼承和指針修復):
g++編譯器實現了在類D中使用兩個虛擬方法表對類B1和B2的多重繼承,它們都是類D的基類。(實現多重繼承還有另外一種方法,但這是最經常使用的)。這使指針修復成爲必要。也叫thunks
思考下面C++ 代碼:
D *d = new D(); B1 *b1 = d; B2 *b2 = d;
當執行這段代碼後,d和b1指向同一存儲位置。b2將指向d+8的位置(8位超出了d的存儲位置)。所以,b2指向d中的「看起來像」B2的實例的區域,即具備與B2的實例相同的存儲器佈局。
4. Invocation(調用):
d->f1()調用的處理方式是經過取消d的D::B1的虛擬指針引用,查找虛擬方法表中f1項,而後取消這段調用代碼的引用。
在單項繼承的例子中(或者是一種語言中的單項繼承),若是虛擬指針始終在d的第一元素(與許多編譯器同樣)這將減小到如下僞C++.
(*((*d)[0]))(d)
*d是指,虛擬方法表中D和[0]指向虛擬方法表中的第一個方法。參數d成爲了指向這個對象的指針。
在更通常的狀況下,調用 B1::f1() 或 D::f2()更復雜些:
(*(*(d[+0])[0]))(d)
(*(*(d[+8])[0]))(d+8)
調用d->f1()是經過將B1指針做爲一個參數。調用d->f2()是經過將B2指針做爲一個參數。這第二個調用須要一個修復(fixup)來產生正確的指針。調用B2::f2是不可能的,由於在D的實現中它已經被重寫了。B2::f2的位置已經不在D的虛擬表中。經過對比,調用d->f0()更簡單:
(*B1::f0)(d)
5. Efficiency(效率):
一個虛擬調用要求至少一個額外的索引取消引用,有時候一個「fixup」的增長,與一個非虛擬調用功能相比,這只是簡單的跳轉到編譯指針。所以調用虛擬方法本質上比調用非虛擬方法慢。一個在1996年作的實現代表,大約6~13%的執行時間是花在簡單的調度到正確的方法。雖然開銷能夠高達50%[4]。虛擬方法的花費在現代的CPU架構上可能不是那麼高,因爲更大的緩存和更好的分支預測。
進一步說,在JIT編譯沒有使用的環境中,虛擬方法調用一般不能內聯。在某些狀況下,編譯器可能會執行稱爲半虛擬化的進程。例如,查找和間接調用被每一個內聯體的條件執行替換,可是這種優化並不常見。
爲了不這些開銷,編譯器一般在編譯時能夠解決的調用時避免使用虛擬表。
所以,以上對f1的調用可能不須要一個虛擬表查詢,由於編譯器可能能夠告訴d在這個點上只能持有D,而且D不重寫f1。或者編譯器(或者優化器)或許能夠檢測到在程序的任何地方沒有B1的子類覆蓋f1。調用B1::f1 或B2::f2可能不會要求一個虛擬表查詢,由於明確地指定了實現。(雖然它仍須要‘this’指針的fixup)
6. Comparison with alternatives(比較和替代):
虛擬表與實現動態調度一般是一個很好的性能交易,可是仍然有適用條件,例如二進制樹分發,具備較高的性能可是成本不一[5]。
然而,虛擬表只容許在特殊參數‘this’上進行單次調度。相比之下,屢次調度(例如在CLOS或Dylan)能夠在調度時考慮全部參數的類型。虛擬表只有在調度被約束到已知的一組方法時才起做用。因此它們能夠放在一個編譯時創建的簡單數組,與鴨式打字語言(如Smalltalk,Python或JavaScript)相反。
提供這些功能中的一個或兩個的語言一般經過在hash表查找字符串或其餘一些等效的方法進行調度。有各類不一樣的技術可使他更快(例如,實習/令牌化方法名稱,高速緩存查找,即時編譯)。
7. See also(參見):
虛擬方法
虛擬繼承
分支表
8. Notes(筆記)
G ++的-fdump-class-hierarchy參數可用於轉儲虛擬方法表以進行手動檢查。 對於AIX VisualAge XlC編譯器,請使用-qdump_class_hierarchy轉儲類層次結構和虛擬功能表佈局。
9. References(參考)
Margaret A. Ellis and Bjarne Stroustrup (1990) The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley. (ISBN 0-201-51459-1)
[1]. Ellis & Stroustrup 1990, pp. 227–232
[2]. Danny Kalev. "C++ Reference Guide: The Object Model II". 2003. Heading "Inheritance and Polymorphism" and "Multiple Inheritance".
[3]. C++ ABI Closed Issues at the Wayback Machine (archived 25 July 2011)
[4]. Driesen, Karel and Hölzle, Urs, "The Direct Cost of Virtual Function Calls in C++", OOPSLA 1996
[5]. Zendra, Olivier and Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java", Pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)
原文來自:https://en.wikipedia.org/wiki/Virtual_method_table#cite_note-4
總結:每一個對象的指針的第一個地址指向一張表(虛擬方法表),這個表裏會有各方法實現的地址
具體:
虛擬方法表:
前提:在new一個對象時會先檢查這個對象有沒有父類,若是有父類就會在這個對象的第一個指針地址上建立一張虛擬方法表。
每個對象地址會有一個指針,這個指針指向一張表(即虛擬方法表),這個表裏會有一個叫call的名字,這個名字會有一個地址,指向這個名字的方法實現。
可是若是子類的方法不是繼承自父類,而是子類本身特有的就不會在虛擬方法表裏存在。
若是子類繼承了父類可是沒有重寫父類的這個方法,這個會記錄在虛擬方法表裏,這個名字指向的地址就是父類這個方法的地址。
public class Callable{ public void call() { System.out.print("say hello"); } public void back(){ System.out.println("say bye"); } }
public class void CallExecutor extends Callable{ @Override public void call() { System.out.println("say hello world"); } public void doOther() { System.out.printlln("do any other thing..."); } }
public class Achieve{ public void getCall(Callable callable){ callable.call(); } }
public void main(String[] args) { Achieve achieve = new Achieve(); Callable object = new CallExecutor(); achieve.getCall(object); }
在這裏,new CallExecutor這個對象時生成一個虛擬方法表
vtable:
VPTR
--------------------------------
call -> 0x00000010-----
public static CallExecutor(CallExecutor this) { public void call() { System.out.println("say hello world") } }
back -> 0x00000050-----
這個地址就是父類back方法的地址(同一個方法的實如今內存中只有一個方法塊)
public static CallExecutor(CallExecutor this) { public void back(){ System.out.println("say bye"); } }
--------------------------------