接著,我們看看虛函數的動態綁定是如何實現的。先看如下代碼:
procedure Test;
var O : T1;
begin
O := T2.Create;
O.func1;
O.func3;
O.Free;
![]()
end;
看著上面的內存布局圖,當執行 O := T2.Create; 后,一個 T1 類型的指針指向 T2 實體。執行O.func1 時,編譯器通過 vptr 找到虛函數表,在虛函數表中定位到了 T2.func1(由于 T1.func1 被“覆蓋”了,因此虛函數表中找不到 T1.func1),于是,T2.func1 被調用,這就是動態綁定!但由于T2 沒有重寫 func3,因此 O.func3 將調用 T1.func3,這一點在虛函數表中也可以很明顯看出來。
好了,說到這里,我想動態綁定已經說的非常清楚了,說明一點,本文雖然以 Object Pascal代碼為例,但其原理對于 C++也同樣有效。C++與Object Pascal(甚至不同C++編譯器之間)的區別僅在于類成員及vptr在內存中分布的位置而已。
那么,最后再談一下 Object Pascal 獨有的 DMT(動態方法表)吧。在VMT中,我們看到,子類的虛函數表完全繼續了父類的虛函數表,只是將被覆蓋了的虛函數的地址改變了。每個子類都有一份自己的虛函數表,可以想象,隨著類層次的擴展,假如類層次非常深,或者子類的數量非常多的話,虛函數表將稱為占用內存量非常大的東西(即所謂的“類爆炸”)。為了防止這種情況, Object Pascal 引入了DMT。對于程序員來說,區別僅在于使用“dynamic”要害字代替“virtual”要害字,所實現的功能也完全一樣。
假如把本文開頭的那段代碼重寫如下(用 dynamic 代替 virtual):
T1 = class
private
member1 : integer;
public
function func1 : Integer; dynamic;
function func2 : Integer; dynamic;
function func3 : Integer; dynamic;
end;
T2 = class(T1)
private
member2 : integer;
public
function func1 : Integer; override;
function func2 : Integer; override;
end;
那么,T1 的內存分布圖沒有改變,而 T2 實例的就不一樣了:

可以看到,在 T2 的動態方法表中,沒有被覆蓋的 T1.func3 消失了。因此:
procedure Test;
var O : T1;
begin
O := T2.Create;
O.func3;
O.Free;
end;
O.func3 這一句代碼將被編譯器做更多的處理:找到 T1 類的 func3 函數的入口地址,然后再調
用。
比較一下 VMT 和 DMT 的區別:
VMT 中的虛函數非常齊全,因此對每個虛函數的入口地址只需要簡單的 [vptr + n] 的運算即可得到,但是 VMT 輕易消耗內存(有冗余)。而 DMT 比較節省空間,但要定位到沒有被覆蓋的函數的入口地址時,將非常耗費時間。
一般情況下,幾乎每個子類都要覆蓋的函數/方法,就將它聲明為 virtual;假如類層次很深,或子類很多,但某個函數/方法只被很少的子類覆蓋,就將它聲明為 dynamic。當然,具體就需要自己把握來選擇了。
進入討論組討論。