接着,我们看看虚函数的动态绑定是如何实现的。先看如下代码:
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。当然,具体就需要自己把握来选择了。
进入讨论组讨论。