再谈多态——向上映射及VMT/DMT(100分)

  • 主题发起人 Nicrosoft
  • 开始时间
N

Nicrosoft

Unregistered / Unconfirmed
GUEST, unregistred user!
再谈多态——向上映射及VMT/DMT
作者:Nicrosoft(nicrosoft@sunistudio.com) 2001.10.9
个人主页:http://www.sunistudio.com/nicrosoft/
东日文档:http://www.sunistudio.com/asp/sunidoc.asp

在《浅谈多态——概念描述》一文中,提到多态的本质就是“将子类类型的指针赋值给父类类型的指
针”。那么,为什麽这种赋值是允许的,或者说是安全的呢?反过来行不行?虚函数的动态绑定是如何实
现的呢?这些问题都将在本文得到解答。

假设有如下代码(Object Pascal语言描述):
T1 = class
private
member1 : integer;
public
function func1 : Integer;
virtual;
function func2 : Integer;
virtual;
function func3 : Integer;
virtual;
end;

T2 = class(T1)
private
member2 : integer;
public
function func1 : Integer;
override;
function func2 : Integer;
override;
end;

最终结果是,T1类的实例的内存分布图如下(仅说明原理,并不表示编译器一定也是如此实现):
___________________ ________________
| vptr |-------> | T1.func1 |
| member1 | | T1.func2 |
~~~~~~~~~~~~~~~~~~~ | T1.func3 |
~~~~~~~~~~~~~~~~
其中,vptr是编译器自动加入的一个成员指针(称为虚指针)。只有存在虚函数或动态函数或纯虚函
数的类才会被编译器加入这个成员指针,该指针指向一个称为“虚函数表”(Object Pascal中成为“虚
方法表”——VMT)的内存区域。虚函数表中,保存了每一个虚函数的入口地址。
T2类的实例的内存分布图如下:
___________________ ________________
| vptr |-------> | T2.func1 |
| member1 | | T2.func2 |
| member2 | | T1.func3 |
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
从图中我们可以知道,子类对象所占的空间大于父类对象所占空间。因此,当发生将子类类型的指针
赋值给父类类型的指针的赋值时(即所谓的“向上映射”),也就是父类类型的指针指向了子类类型的对
象所占的内存空间,那么,很显然,可以保证父类类型指针的可访问范围都是有效,所以这种“向上映
射”是绝对安全的(所谓“向上”是指类层次的上下关系,父类在上,子类在下)。这种赋值是得到编译
器认可的。
也可以很容易得出结论,“向下映射”则未必安全(除非程序员真正知道指针所指对象的实际类
型)。因此,这种赋值是不被编译器允许的,当然,程序员可以通过类似 T1(Obj) 的形式进行强制类型
转换,但这种强制类型转换很不安全(可以发生在任何类和类之间),Object Pascal推荐使用 as 算符
进行类型之间的转换,如: (Obj as T1),使用 as 算符,编译器会检查对象类型和目标类型是否相容。
如果相容,转换被允许,否则编译出错。
接着,我们看看虚函数的动态绑定是如何实现的。先看如下代码:
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 实例的就不一样了:
___________________ ________________
| dptr |-------> | T2.func1 |
| member1 | | T2.func2 |
| member2 | ~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~
可以看到,在 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。当然,具体就需要自己把握来
选择了。
 
Nicrosoft兄也来dfw了,dfw又多了一个高手,呵呵。
真的很巧,我昨天刚刚看了关于vmt的一些文档,有点不清楚,今天又看到你的这个帖:)
非常感谢:)
 
___________________ ________________
| vptr |-------> | T1.func1 |
| member1 | | T1.func2 |
~~~~~~~~~~~~~~~~~~~ | T1.func3 |
Nicrosoft兄,小弟有些不明白, ~~~~~~~~~~~~~~~~
我看到一些文档上说一个类都只占四个字节的指针,被创建后就指向vmt,但是类的数据成员
存放在什么地方呢?在你的图示中好象是类被创建在一个堆中,堆的前四个字节指想vmt,
其余的空间存放数据成员,到底那种说法是对的呢,还是小弟我理解错误?
 
类成员中包含了vptr(一个指针),以及其他的成员变量,至于vptr放在最前面,
还是放在最后面,还是放在中间,并没有规定。Object Pascal的标准我不太清楚,
但大多数的编译器(包括各种C++编译器)都放在最前面。
 
不过,我在delphi中试了一下,用sizeof查看,的确只有一个指针大小。
你看到的文档能不能告诉我在哪里看到的?我想看一下,Delphi的编译器具体如何实现的。
 
在Delphi中,对象[red]实例[/red]的大小应该用InstanceSize来获得。
我在Delphi与VC之间传递对象时发现,InstanceSize比你显式定义的成员的大小总和要大4Bytes,
这多出来的4Bytes正是指向VMT表的指针(位于对象实例空间的头部,和C++一样)。
 
to Nicrosoft
你的email,我发给你,李维写的《delphi原子世界》
 
nicrosoft@sunistudio.com
thank u
 
to 不不:
mail to me a copy : bpearl@21cn.com
or give us ado
wnloading address. thanks.
 
to 不不:
mail to me a copy : yxyletter@21cn.com
or give us ado
wnloading address. thanks.
 
呵呵,Nicrosoft跑大富翁布道来了。
 
学着点。。。
 
不不兄:
《delphi原子世界》请给我mail一份,多谢了!
yufan.zhou@263.net
 
多人接受答案了。
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
I
回复
0
查看
423
import
I
I
回复
0
查看
432
import
I
D
回复
0
查看
1K
DelphiTeacher的专栏
D
顶部