当前位置:编程文档 >> DELPHI >> 关于delphi编程的多态实现
首页

关于delphi编程的多态实现

所属类别:DELPHI
推荐指数:★★★★
文档人气:233
本周人气:2
发布日期:2008-3-4

今天看了才真正的明白什么是多态性,其实就是覆盖。基类中有一个方法,必须是虚方法,在其子类中用override进行覆盖。

在静态方法中,定义一个和基类一样的方法,一个变量为基类类型,子类对象对其赋值,方法为基类方法,只有变量为子类类型方法才是子类方法,但用override覆盖后,对象是什么类就用什么类的方法。 

摘录了一篇文章

多态的概念与接口重用  

首先,什么是多态(Polymorphisn)?按字面的意思来讲,就是“多种形状”。笔者也

没有找到对多态的非常学术性的描述,暂且引用一下Charlie Calvert对多态的描述——多态

性是允许用户将父对象设置成为与一个或更多的它的子对象相等的技术,赋值之后,基类

对象就可以根据当前赋值给它的派生类对象的特性以不同的方式运作。

更简单地说就是:多态性允许用户将派生类类型的指针赋值给基类类型的指针。多态

性在Object Pascal中是通过虚方法(Virtual Method)实现的。

什么是“虚方法”?虚方法就是允许被其派生类重新定义的方法。派生类重新定义基

类虚方法的做法,称为“覆盖”(override)。

这里有一个初学者经常混淆的概念:覆盖(override)和重载(overload)。如前所述,

覆盖是指派生类重新定义基类的虚方法的方法。而重载,是指允许存在多个同名函数,这

些函数的参数表不同(或许是参数个数不同,或许是参数类型不同,或许两者都不同)。

重载的概念并不属于“面向对象编程”。重载的可能的实现是:编译器根据函数不同的参

数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器

来说)。例如,有两个重载的同名函数

function func(p : integer) : integer; overload;

function func(p : string) : integer; overload;

那么编译器做过修饰后的函数名可能是:int_func、str_func。如果调用

func(2);

func(′hello′);

那么编译器会把这两行代码分别转换成:

int_func(2);

str_func(′hello′);

这两个函数的调用入口地址在编译期间就已经静态(记住:是静态!)确定了。这样

的确定函数调用入口地址的方法称为早绑定。

而覆盖则是:当派生类重定义了基类的虚方法后,由于重定义的派生类的方法地址无

法给出,其调用地址在编译期间便无法确定,故基类指针必须根据赋给它的不同的派生类

指针,在运行期动态地(记住:是动态!)调用属于派生类的虚方法。这样的确定函数调

用地址的方法称为晚绑定。引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,

它就不是多态”。

..注意:重载只是一种语言特性,与多态无关,与面向对象也无关!

多态是通过虚方法实现的,而虚方法是通过晚绑定(或动态绑定)实现的。

其次,多态的作用是什么呢?前两节已经讲到,封装可以隐藏实现细节,使得代码模

块化;继承可以扩展已存在的代码模块,它们的目的都是为了代码重用。而多态则是为了

实现另一个目的——接口重用。

什么是接口重用?举一个简单的例子,假设有一个描述飞机的基类:

 

type

 TPlane = class

 protected

 FModal : String; // 型号

 public

 procedure fly(); virtual; abstract; // 起飞抽象方法

 procedure land(); virtual; abstract; // 着陆抽象方法

 function modal() : string; virtual; // 查寻型号虚方法

 …… // 其他可能的操作

 end;

然后,从TPlane派生出两个派生类,直升机(TCopter)和喷气式飞机(TJet):

 TCopter = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 function modol() : string; override;

 …… //其他可能的操作

end;

 TJet = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 …… //其他可能的操作,没有覆盖modal()

 end;

TPlane类的声明中,fly和land方法都是被声明为virtual和abstract的,这是向编译器

指出这些方法是抽象(纯虚)的,也就是在TPlane类中不提供这些方法的实现,而派生类

则必须实现它,即规定了一套接口。凡是含有abstract方法的类被称为“抽象类”,永远无

法创建抽象类的实例对象。抽象类是被用来作为接口的。

现在,假设要完成一个机场管理系统,在有了以上的TPlane之后,再编写一个全局的

函数g_FlyPlane(),就可以让所有传递给它的飞机起飞:

procedure g_FlyPlane(const Plane : TPlane);

begin

 Plane.fly();

end;

是的,仅仅如此就可以让所有传给它的飞机(TPlane的派生类对象)正常起飞!不管

是直升机还是喷气式飞机,甚至是现在还不存在的、以后会增加的飞碟。这是因为,每个

派生类(真正的飞机)都可以通过“override”来定义适合自己的起飞方式。

可以看到,g_FlyPlane()函数接受的参数是TPlane类对象的引用,而实际传递给它的都

是 TPlane的派生类对象。现在回想一下本节开头所描述的“多态”:多态性是允许将父对

象设置成为与一个或更多的它的子对象相等的技术,赋值之后,父对象就可以根据当前赋

值给它的子对象的特性以不同的方式运作。很显然,

parent := child;

就是多态的实质!这里的飞机类(TPlane)作为一种接口,而该接口就是被重用的目标。

多态的本质就是“将派生类类型的指针赋值给基类类型的指针”(在Object Pascal中

是引用),只要这样的赋值发生了,就是在应用多态了,因为在此实行了“向上映射”(“上

下”是指类继承层次关系)。

应用多态的例子非常普遍。在Delphi的VCL类库中,最典型的就是:TObject类有一

个虚拟的Destroy析构函数和一个非虚拟的Free方法。Free方法中首先判断对象本身是否

为nil,保证不为nil时便调用Destroy。对任何对象(都是TObject的派生类对象)调用其

Free();方法,但执行的都是TObject.Free();(因为TObject.Free()为非虚拟方法,无法被覆盖),

然后由它调用被每个类重定义了的析构函数Destroy();(因为Destroy()为虚方法,派生类可

以覆盖),这就保证了任何类型的对象都可以正确、安全地被析构。

因此,在定义自己的类时,如果有析构函数存在,就必须在它的声明之后加上override

关键字。否则会发生什么呢?

还拿刚才的飞机类作为例子,有一个飞机销毁站,这个销毁站有一个函数:

procedure DestroyPlane(var Plane : TPlane)

begin

 Plane.Free();

 Plane := nil

end;

将正常的飞机(析构函数带有override的飞机)传给它,编译器都会正常地调用飞机

的析构函数用以将飞机拆解开,将资源正常回收。

如果建造的飞机的析构函数没有被指明override关键字,那么,将飞机传递给这个

DestroyPlane()的函数时,编译器调用的绝对不是用户所给飞机定义的析构函数,而会是

TPlane.Destroy()。能指望TPlane的析构函数会好好拆解飞机吗?祈祷吧,别把油箱弄爆

炸了。

也就是说,在被执行了

parent := child;

之后,无论调用

parent.Free();

还是调用

child.Free();

都应该产生同样的结果,从语义上来说,这两行代码必须做相同的事情。当然,这样的情

况不仅仅只对于析构函数而言,任何想要使通过基类对象指针做到的事情与通过派生类对

象指针所做的相同,就要在基类中将这个方法声明为virtual,在派生类中将该方法声明为

override。

..注意:给自己的析构函数加上override声明!

多态的实现与VMT/DMT

 

多态的本质是“将派生类类型的指针赋值给基类类型的指针”。那么,为什么这种赋

值是允许的,或者说是安全的呢?

从语义上来讲,继承所表现的是“是一种”的关系,也就是说,每个派生类对象必定

“是一种”基类对象。所以,任何向基类类型的请求,派生类对象都可以无条件地正常处

理。因为直升机“是一种”飞机,喷气式飞机也“是一种”飞机,所以所有对飞机的操作

请求,它们都应该可以正常处理。

从语言上来讲,由于派生类通常比基类拥有更多的数据成员而绝对不会更少,派生类

对象所占的内存空间必定大于或等于基类对象所占的内存空间。因此,将基类类型的指针

指向派生类类型的对象时,在指针的可视范围中的内存必定是可用的,这一部分内存空间

必定是属于对象的,所以这种赋值行为是合法的、安全的,并且得到编译器认可的。

例如,如下的两个类:

 TBase = class

 private

 FMember1 : Integer;

 FMember2 : Integer;

 end;

 TDerived = class(TBase)

 private

 FMember3 : Integer;

 end;

当TBase类型的指针指向其派生类类型TDerived的对象后:

var

 Parent : TBase;

 Child : TDerived;

Begin

 Child := TDerived.Create();

 Parent := Child; //当执行这行代码之后……

 // 以上两行代码也可以简化为Parent := TDerived.Create();

 …… //之后的代码省略

End;

TBase类型的指针(Parent指针)指向了Child对象实体所在的内存首地址。在2.3节

中说过,每个派生类对象实体中都包含了一个完整的基类对象实体。此处的Parent指针可

以访问的范围,正是这个完整基类(TBase)对象实体的大小。因此,Parent指针始终可以

合法地访问其所指向的内存空间。

Parent指针的可视范围如图2.5所示。

指向VMT的指针

Child对象实体

FMember1

FMember2

FMember3

VMT

Parent指针

的可视范围

图2.5 基类指针可视范围演示

在图2.5中又一次看到了VMT,VMT究竟是何方神圣呢?为什么每个对象都会有一

个指向VMT的指针呢?这些问题可以在了解虚方法的动态绑定实现机制中找到答案。搞

清这些,便会清楚多态是如何实现的了。

当创建一个类的实例之后,编译器会在该对象的内存空间的首4个字节安插一个指针,

该指针所指向的地址称为VMT(Virtual Method Table,虚方法表),这个表中存放了该类

的所有虚方法的入口地址。在Object Pascal中,所有类实例都会有这么一个指向VMT的

指针。如果没有在类中声明虚方法,则该指针为nil。

还是以前面所说的飞机抽象类和直升机类为例:

 TPlane = class

 protected

 FModal : String;

 public

 procedure fly(); virtual; abstract; // 起飞抽象方法

 procedure land(); virtual; abstract; // 着陆抽象方法

 function modal() : string; virtual; // 查寻型号虚方法

 …… // 其他可能的操作

 end;

 TCopter = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 …… // 其他可能的操作,没有覆盖TPlane.modal()

 end;

在一个全局函数中用飞机类型来创建直升机实例:

procedure g_CreateACopter(var Plane : TPlane);

begin

 Plane := TCopter.Create;

end;

当执行Plane := TCopter.Create之后,一个直升机实例就被创建了,并且Plane指针指

向了它,如图2.6所示。

Plane指针 指向VMT的指针

FModal

直升机对象实例直升机类的VMTTCopter.fly()

TCopter.land()

TPlane.modal()

TCopter没有覆盖

TPlane.modal()

图2.6 plane指针指向直升机对象实例

没有被派生类覆盖的方法,编译器会将基类的该方法的实现的入口地址填入派生类的

VMT中。如图2.6所示,直升机类(TCopter)覆盖了其基类(TPlane)的虚方法fly()和land(),

因此在TCopter的VMT中,fly和land被确定为TCopter的实现方法的入口地址。但由于

TCopter没有覆盖TPlane的虚方法modal(),则在VMT的modal()项中被填入了TPlane.modal()

的入口地址,即基类中该方法的入口地址。

被派生类覆盖的方法,则会将派生类实现的方法的入口地址填入VMT中以取代基类

被覆盖的方法。

这就是“晚绑定”或“动态绑定”!

对照图2.6来看,当基类类型的指针指向了直升机实例对象后,可以通过基类类型的

指针来让这架直升机起飞:

plane.fly();

编译器通过plane所指对象的“指向VMT的指针”可以定位到TCopter.fly()的地非曲

直址,由此便可以找到属于直升机的fly()方法,而得以以直升机的起飞方式来让一架直升

机起飞。

虽然动态绑定看起来并不太复杂,但意义重大!如果没有动态绑定,那么,试想一下,

以喷气式飞机的起飞方式来让一架直升机起飞会发生什么?这可关系到飞行员的安全啊。

到这里,读者对动态绑定的实现应该清楚了。细心的读者可能又会发现一个问题,

TPlane中的析构函数也是virtual的,为什么没有出现在VMT中呢?

当然,并非VMT中没有析构函数,只能说它没有出现在我们所能见到的VMT中。之

前所说的“指向VMT的指针”所指向的VMT,其实只是真正的VMT的一部分,也就是

用户定义的第一个虚方法的位置。如果以这个位置作为原点,向正方向即刚才所说的VMT。

而向负方向,则是语言定义的另一些类信息所在的地址,析构函数地址就被放在了负方向

上了。

Delphi之所以这么做,是为了使得Object Pascal的VMT与C++的以及COM的vtable(虚函数表)兼容。

这里给出完整的VMT的地址表(来自Delphi6 Help),不过这些内容仅能作为参考,

因为Borland并不承诺将来不会改变这个格式,如表2.1所列。

表2.1 Delph6 VMT

偏移地址

类 型

描 述

-76

Pointer

pointer to virtual method table (or nil)

-72

Pointer

pointer to interface table (or nil)

-68

Pointer

pointer to Automation information table (or nil)

-64

Pointer

pointer to instance initialization table (or nil)

-60

Pointer

pointer to type information table (or nil)

-56

Pointer

pointer to field definition table (or nil)

-52

Pointer

pointer to method definition table (or nil)

-48

Pointer

pointer to dynamic method table (or nil) (指向DMT的指针)

-44

Pointer

pointer to short string containing class name

-40

Cardinal

instance size in bytes

-36

Pointer

pointer to a pointer to ancestor class (or nil)

续表

偏移地址

类 型

描 述

-32

Pointer

pointer to entry point of SafecallException method (or nil)

-28

Pointer

entry point of AfterConstruction method

-24

Pointer

entry point of BeforeDestruction method

-20

Pointer

entry point of Dispatch method

-16

Pointer

entry point of DefaultHandler method

-12

Pointer

entry point of NewInstance method

-8

Pointer

entry point of FreeInstance method

-4

Pointer

entry point of Destroy destructor

0

Pointer

entry point of first user-defined virtual method

4

Pointer

entry point of second user-defined virtual method

可以看到,偏移地址-28 ~ -4所存放的都是TObject的虚方法地址,当然析构函数也在

其中。

最后,有必要再谈一下Object Pascal所独有的DMT(动态方法表)。

在表2.1的偏移地址为-48处是一个指向DMT的指针,它是干什么用的?它和VMT

有什么关系?

在VMT中可以看到,派生类的虚方法表完全继承了基类的虚方法表,只是将被覆盖

了的虚方法的地址改变了。基类和每个派生类都有一份自己的虚方法表。可以想象,随着

类层次的扩展,虚方法表将耗费非常大的内存空间。为了防止这种情况,Object Pascal引

入了“dynamic”的概念。对于程序员来说,dynamic方法和virtual方法实现相同的功能,

只是声明的关键字不同:

Procedure fly(); dynamic; // 是dynamic而不是virtual

被声明为dynamic的方法,其入口地址将被放在DMT中。DMT和VMT的区别在于:

对于派生类没有覆盖的方法,这些方法的入口地址不会出现在DMT中,编译器要通过基

类的信息来寻找它们的入口地址。

如果将TPlane的抽象方法land和虚方法modal改成dynamic,即:

 TPlane = class

 protected

 FModal : String;

 public

 procedure fly(); virtual; abstract; // 仍然保持virtual

 procedure land(); dynamic; abstract; // 将virtual改成dynamic

 function modal() : string; dynamic; // 将virtual改成dynamic

 …… // 其他可能的操作

 end;

 TCopter = class(TPlane)

 public

 constructor Create();

 destructor Destroy(); override;

 procedure fly(); override;

 procedure land(); override;

 function modal() : string; // 不覆盖Plane.modal();

 …… // 其他可能的操作

 end;

则TCopter的VMT/DMT会变成如图2.7所示的样子。

对比图2.6和图2.7可知,由于DMT中不会出现没有被派生类覆盖的基类dynamic方

法,因此DMT会比VMT节省空间(大多数情况下)。当基类有许多虚方法,而派生类只

覆盖很少几个时,区别尤其明显。当派生层次越来越深,派生类数量越来越多,DMT就能

节省更多的内存空间。但是DMT中对基类的动态方法的寻址不是直接进行的,因此dynamic

方法的寻址比virtual方法要慢许多。

Plane指针 指向VMT的指针

FModal

直升机类的VMT

直升机对象实例

TCopter.land()

……

指向DMT的指针

……

……

……

TCopter.fly()

直升机类的DMT

……

DMT中没有了未被覆

盖的TPlane.modal()

图2.7 直升机类的VMT/DMT

virtual和dynamic的区别仅在于编译器采用不同的晚绑定策略而已,对于程序员来说,

它们的功能相同。

如何取舍就看实际的需求了,一般情况下,几乎每个派生类都要覆盖的方法,将它声

明为virtual;如果类层次很深,或派生类很多,但某个方法只被很少的派生类覆盖,则将

它声明为dynamic。

另外需要注意的是,只有VMT才与C++、COM的vtable兼容,因此当需要这样的兼

容性时,只能使用virtual。

2.5 小 结

传统的说法是,封装、继承、多态是面向对象编程的三个基本特性。实际上,封装只

是抽象数据类型(ADT),有了继承才能被称为面向对象。而继承的存在,除了扩展现存

类的功能外,另一个更重要的作用就是作为多态存在的基石。

多态是一种能够带来灵活性的东西,它使得通过接口重用来实现代码重用。可以毫不

夸张地说,不领会多态,不明白晚绑定,就不可能明白什么是面向对象!因为只有在会用

virtual后,才是真正在用面向对象的典范(paradigm)思考……

文档说明:

     

相关文档


读取评论列表……