第 6章 Delphi中类的应用
内容提要
? 面向对象的相关概念
? 类的定义
? 类的成员
? 类的特性
? 共同祖先 Tobject简介
? Delphi中的 VCL
? 自定义类
6.1 面向对象的相关概念
1.对象
对象是现实世界中一类具有某些共同特性的事物的
抽象。对象是构成系统的元素,是组成问题域的事物。
这里问题域指应用系统所要解决和问题。小到一个数据,
大到整个系统都是对象。对象不仅仅是物理对象,如写
字台、手机等,还可以是某一类概念实体,如操系统中
进程、室内照明的等级等都是对象。
2.消息
消息,就是指 Windows发出的一个通知,告诉应用程序
某个事情发生了。例如,单击鼠标,改变窗口尺寸,按下
键盘上的一个键都会使 Windows发送一个消息给应用程序。
消息本身是作为一个记录传递给应用程序的,这个记录中
包含了消息的类型及其他信息。例如,对于单击鼠标所产
生的消息来说,其记录类型为 TMsg,同时消息记录中还包
含了单击鼠标时的坐标信息。
对象进行处理及相互之间的联系,都只能通过消息传
递来实现,发送消息的对象叫发送者,接受消息的对象叫
接收者。发送者可以同时向各个对象传递消息;接受者可
同时接受多个对象发来的消息。
3.类
类定义的是对象的类型,是对一组性质相同的对象的描述,
它用于描述对象的所有性质,包括外部特性和内部实现。通过
消息及相应的处理能力的描述,定义对象的外部特性;通过
内部状态和处理能力的实现来描述定义对象的内部实现。在
程序运行时,类被作为样板建立对象。对象和类的关系,就
如同前面介绍的变量和类型的关系。
4.继承
所谓继承就是一个新的类类型,不必什么都重新定义,
只需要继承一个已有的类型再加上自己的成员就构成一个新
的类类型。继承是一个对象可以获得另一个对象特性的机制。
类是一种层次结构,类的上层可以有子类。当某个类定义了
某个特征后,所有在它下面的类都不得包启了该特征。类此
子类直接继承其父类的全部描述,这叫传递性。类可以有多
个父类,这叫多重继承,如果只能有一个父类,叫简单继承。
Delphi中的类是简单继承。
6.2 类的定义
从程序设计的角度讲,类是一种数据类型,
是一种特殊的数据类型,不过类定义了一种由字
段、属性和方法 3部分构成的数据结构。声明一个
类的过程,也就是创建字段( Fields)、属性
( Property)、方法( Method)的过程。
?字段:是类的内部数据变量。
?属性:是类提供给外部使用的数据变量。
?方法:是类中定义的函数和过程。
三者统称为类的成员。类成员字段、属性与方法
在类中有不同的作用。下面首先学习如何定义类。
6.2.1 类的定义
类类型声明的一般形式,
Type
类名 =Class(父类名)
[类成员 ]
End;
( 1)类名可以是任何合法的标识符,不过 Delphi有一个约定,使
用大写字母 T作为前缀来标识类类型。
( 2) Class是保留字,它表明声明的类型是一个类类型。
( 3) Class后的括号内为父类名,表明当前声明的类派生于父类名
指定的类。
( 4) Class后的括号是可选的,如果不指明其父类则表示新声明的
类直接从 Dephi的 Tobject类继承而来。
6.2.2 类的实例化
类的实例化就是利用类的方法创建对象的过程。方法,
( 1)首先声明对象变量,形式如下,
Var对象变量名:类名,
( 2)创建对象变量,分两种情况。
①如果类是直接由 Tobject继承而来,就用如下形式创建
对象,
对象变量名,=类名,Create;
② 如果类重载或覆盖了 Tobject的构造方法( Create),
则创建对象的语句形式如下,
对象变量名,=类名,构造方法 (参数表 );
例如,如下代码就是 Taverage中创建一个对象的过程,
Var average:Taverage; //声明对象变量
average:=Taverage,Create; //创建对象实例
6.2.3 类运算符
1.类型判断运算符 is
is 运算符用来检测一个类是否与另一个类兼容,即左操作符是否是右
操作规程符的同类或子类,其语法形式如下,
对象变量 is 类名
如果返回值为 True,那么对象变量是类或其派生类的一个实例。如
果对象为 nil,返回值为 false。在 Windows程序设计中,经常使用 is运算
符判断组件的类型,例如,
For I:0 to ComonemtCount-1 do
If Components[i] is Tedit then
Tedit(Components[I]).Text:=“;
以上代码,逐一检测窗体中的组件是否为编辑框,如果某组件是编辑
框,则清空该编辑框 。
2,类型强制转换运算符 as
as运算符是用来进行强制类型转换的,其语法形式如下,
对象变量 as 类名
as在进行类型强制转换时首先测试,然后进行转换,若
转换不成功,则引起异常 ElnvalidCast。
例如,Components[i] as Tedit 这条语句相当于以下
语句,
If Components [i] is Tedit then
Tedit(Components [i])
Else
Raise ElnvalidCast.Create;
6.2.4 类指针
类引用即是指向类的指针,而不是指向对象的指针,定义
类引用类型和类引用变量的语句格式如下,
Type类引用类型 =class of 类名
Var 类引用变量名:类引用类型;
例如,在 Taverage中定义
Type Taverage Ref=Class of Taverage;
var aRef,Taverage Ref;
6.3 类的成员
类成员由字段、属性和方法组成。
6.3.1 类的字段
类的字段也称为数据域,用来存储一个实例(对象)的信
息,基本上可看成是一个变量,它可以是一组 Delphi支持的
类型变量的集合。
6.3.2 类的属性
类的属性用来描述类的实例(对象)的特征,它是访问对象
数据的接口。属性声明中含有访问指令符( Read,Write),访
问指令符用来决定属性的读写方法。属性控制如何使用属性过
程设置或返回一个值。
由以上描述可知,属性在类中的功能主要有两个:一个是设
置属性值,一个是返回属性值,与这两个功能对应的有两个
存取程序,这两个存取程序分别用 Read和 Write关键字来定义,
Read块用于获取属性的值,而 Write块用于设置属性的值。也
可以忽略一个块来创建只读属性或只写属性。不过属性至少
要包含一个块才是有效的。
定义属性用到关键字 Property,属性的一般定义形式,
Property属性名:属性类型 [Read字段或方法 ][Write字
段或方法 ] [Default默认值 ];
6.3.2 类的属性
6.3.3 类的方法
类的方法就是在类中定义的一个过程或函数。类的方法
需要先声明后实现。方法的声明在类的声明处进行,且只包
含过程或函数的首部,而方法的实现要在单元的实现部分完
成。 Delphi中有以下几种方法:一般方法、构造方法、析构
方法及类方法。
1.一般方法
此类方法在类内定义,在单元内实现,既可以是过程也可以是函数。
( 1)定义一般方法的语句格式如下,
Type
类名 =class(父类名)
保护方式关键字( Public,Private等)
Procedure方法名(参数表);
Function方法名(参数表):返回值类型;
…,
End;
( 2)实现一个方法的语句格式如下,
Procedure类名.方法名 (参数表 );
或者
Function 类名.方法名(参数表):返回值类型;
{常量、变量等定义 }
Begin
{执行语句; }
End;
( 3)调用一个方法的语句格式如下,
对象变量.方法名(实际参数);
注,一般方法的实现方法名前面要加上类名的限定。
2.构造方法
构造方法是一种特殊的方法,用来创建类的对象并对其进
行初始化。在声明类的对象后,并没有创建该对象,只是定
义了指向该类类型的一个指针,对象的创建和初始化工作是
由类的构造方法来完成。在定义构造方法时,使用保留字
constructor,名称通常为 create。
(1)定义构造方法的语句格式如下,
Constructor构造方法名(参数表);
( 2)实现构造方法的语法如下,
Constructor类名,构造方法名 (参数表 );
( 3)调用构造方法的语句格式如下,
对象变量名,=类名,构造方法名(参数表);
3.析构方法
析构方法用来释放类的对象,并且释放对象中的其他数据
结构。在定义析构函数时,使用保留字 Destructor,函数名通
常为 Destroy。
( 1)定义析构函数的语句格式为,
Destructor析构方法名(参数表);
( 2)实现析构方法时按照如下语法,
Destructor类名,析构方法名(参数表);
( 3)调用析构方法的语句格式如下,
对象变量名,析构方法(参数表);
析构方法不是必须的,只有在构造方法中分配了内存、使
用了资源、打开了文件或数据库,才需要析构方法做善后处理
工作。
4.类方法
Object pascal中还有一种称为类方法的特殊方法,类方法
跟构造有些相似,其相似之处在于它们都能由类来引用,而不
必先创建一个对象实例,也就是说类方法不依赖于任何类的具
体实例。一般方法只能被类的实例调用,而类方法既可以被对
象实例调用,也可以被类本身引用。类方法只是表明这个方法
在逻辑上与这个类有联系。
类方法可以是过程,也可以是函数,类方法在类结构中定
义,与一般方法的区别是在关键 Procedure或之前加一个 Class
关键字。
例如,
Type TClass=Class
Class Function GetClassName:String;
End;
上例中, 声明了一个类方法, 它是一个返回类型为字符串
的函数 。
在程序中, 可以直接由类来引用类方法, 例如,
Var MyString:String;
MyString:=TClass.GetClassName;
( 1)定义类方法的语句格式如下,
Class Procdure 类名,类方法名(参数表);
或者
Class Function 类名,类方法名(参数表);
( 2)调用类方法的格式如下,
类名,类方法名(参数表);
或者
对象变量名,类方法名(参数表);
6.3.4 方法的类型
一个类中的方法,可以在声明时使用不同的指示字指定为静
态的、动态的、虚拟的、抽象的、消息处理程序的方法。这些指
示字是 Static,Dynamic,Virtual,Message,Abstract,如果不加
方法指示字,系统默认为静态方法( Static)。
1.静态方法
在声明方法时如果没有使用指示字,该方法即为静态方法。
当一个静态方法被调用时,方法名前的对象变量的类名决定调
用的是哪个类的方法。例如以下代码中的 Fly方法即为静态方法。
Type
TPlane=class
Procedure Fly;
End;
Tjet =class(Tplane)
Procedure Fly;
End;
2.虚拟方法
虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定
的,而是在程序运行期间根据调用这个虚拟方法的对象实例来确定的,这种方
法又称为滞后联编。
虚拟方法通过指示字 Virtual来声明,声明的一般形式如下,
procedure 方法名(参数表); Virtual;
function 方法名 ( 参数表 ),返回值类型; Virtual;
Constructor 方法名 ( 参数表 ) ; Virtual;
Destructor 方法名 ( 参数表 ) ; Virtual;
【 例 6.4】 在 Tplane( 父类 ) 中声明虚拟方法 Fly,在子类 Tcopter和 Tjet中覆
盖父类中的同名方法 Fly。
Type
Tplane = class
procedure fly; virtual //声明虚拟方法
end;
然后, 从 Tplane派生出两个子类, Tcopter和 Tjet,
Tcopter = class(Tplane)
private
fModal, String;
public
procedure fly; override; //覆盖父类中的方法
end;
Tjet = class(Tplane)
private
fModal, String;
public
procedure fly; override; //覆盖父类中的方法
end;
调用虚拟方法,
var
plane:Tplane,
beign
plane:=Tcopter.Create;
plane.Fly; //调用 Tcopter类的 Fly方法
plane.destroy;
palne:=Tjet.create;
plane.Fly; //调用 Tjet类的 Fly方法
plane.Destroy;
end;
3.动态方法
动态方法的使用与虚拟方法相同,只是在内部实现上,虚拟方法速度较快,
但存储空间耗用大,而动态方法的存储空间耗用少,但降低了速度。
动态方法通过指示字 Dynamic来声明, 一般形式如下,
procedure 方法名 ( 参数表 ) ; Dynamic;
function 方法名 ( 参数表 ),返回值类型; Dynamic;
Constructor 方法名 ( 参数表 ) ; Dynamic;
Destructor 方法名 ( 参数表 ) ; Dynamic;
? 如下代码声明了一个动态方法 fly,
Type
Tplane = class
procedure fly; Dynamic ; //声明动态方法
end;
4.抽象方法
所谓抽象方法,首先必须是虚拟的或动态的,其次在它所在类中只
能声明而不能定义,只能在派生类中定义它。因此定义一个抽象
方法,只是定义它的接口,而不是定义底层的操作。 声明抽象方法
使用 abstract指示字,例如,
type
Tdesign=class(Object)

Procedure Draw:Virtual;abstract;

end;
5.消息处理程序的方法
在方法的调用约定之后加上一个 message,就可以定义一个消息
处理程序方法,消息处理程序方法主要用于相应并处理某个特定
的事件。
声明消息处理方法使用 message指示字, 例如,
type
Tbox=class(Tcustomcontrol)

Private
Procedure WMChar(Var Message:TWMchar); message WMCHAR;

end;
6.3.5 覆盖与重载
由以上学习可体会到:虚拟方法就是允许被其子类重新定
义的成员函数。而子类重新定义父类虚拟方法的做法,称为
,覆盖,,或者称为, 重写, 。
重载,是指允许存在多个同名函数,而这些函数的参数表
不同。其实,重载的概念并不属于, 面向对象编程,,重载的
实现是编译器根据函数不同的参数表,对同名函数的名称做修
饰,然后这些同名函数就成了不同的函数。如,有两个同名函
数,function func(p:integer):integer; 和 function
func(p:string):integer;。那么编译器做过修饰后的函数名称
可能是这样的,int_func,str_func。对于这两个函数的调用,
在编译器间就已经确定了,是静态的。
6.4 类的特性
类有三大特性,
?封装性
?继承性
?多态性
6.4.1 类的封装性
类是由封装在一起的数据和方法构成的 。
所谓 封装 指的是一个类中的有些成员对其他类来说是不可能
直接访问的, 这些成员只能由类本身的方法或属性来访问 。
Object Pascal中通过控制类成员的可见性来实现类成
员的封装 。 一个类中, 成员的可见性通过 5个保留字
来控制, 这 5个保留字是,private,public,protected、
published和 automated。
1.Private
保留字 Private表示一个类的成员是这个类所私有的, 在声明
这个类的单元或程序之外是不可见的 。 也就是说, 在包含这
个类的单元中, 可以对定义为私有的字段和方法进行访问,
而对于其他类包括它的派生类, Private部分声明的成员都是
可见的, 这就是面向对象编程中的数据保护机制 。
2.Public
保留字 Public表示一个类的成员是公有的,在声明这个类
的单元或程序之外是可见的,这意味着在程序的任何地方都可
以直接访问这些成员
3.Protected
保留字 Protected表示一个类的成员是受保护的,受保护的
成员只能被当前类和当前类的子类所访问。
在 Protected部分声明的成员通常是方法,这样在派生类中
访问这些方法,而不需要知道这些方法实现的细节。
4.Published
保留字 Published声明的成员是发行类型的成员。从成员的可
见性来说,发行成员是最高的。公有成员和发行成员都是公共的,
能够被其他类的实例引用,也就是在运行期间可以随意访问。两
者的区别在于公有成员在运行期间是可以访问的,而发行成员在
设计期间和运行期间都可以被访问。
5.Automated
保留字 Automated与 Public基本相同,惟一的区别在于声明
为 Automated的的成员会产生自动化类型信息。自动化类型信息
是自动化服务器所要求的。因此,只有定义或应用自动化对象时
才需要使用 Automated成员类型。
6.4.2 类的继承性
类类型具有可继承性。
所谓 继承 就是一个新的类类型,不必什么都重新定义,
只需要继承一个已有的类型再加上自己的成员就构成
一个新的类类型。
被继承的类称为 基类,继承下来的类称为 派生类,基
类的成员自动成为派生类的成员。 类的继承具有传递
性,例如假设 T3继承了 T2,而 T2又是继承了 T1,可以
认为 T3也继承 T1。在 Delphi中,所有的类都是从一个
共同的类 TObject继承下来的。
6.4.2 类的继承性
在 Delphi中继承性很容易实现, 只需在定义子类时指出它希
望继承的父类即可 。 例如, 在建立新窗体时, Delphi会自
动创建 Tform1类, 代码如下,
type
TForm1 = class(TForm)
private
{ Private declarations }
public
{ Public declarations }
end;
这表明, Tform1是 Tform的子类, 我们可以在不添加任何组
件的情况下改变窗体的属性, 如 Caption,Width,Height
等, 这些属性都是从 Tform类继承来的 。
6.4.3 类的多态性
相同的函数调用被不同的对象接受时,会导致完
全不同的行为,这种现象称为多态性。利用多态性,
程序中只需进行一般形式的函数调用,函数的实现细
节留给接受函数调用的对象。多态性是问题求解的面
向对象方法的一种关键性能。
在 Object Pascal中,多态性是通过虚拟方法或动态
方法实现的。
参见:例 6-5
例题中的关键问题是,当程序调用 Fly时,究竟调用的
是哪个 Fly,是基类的 Fly还是派生类的 Fly 呢?
6.5 共同祖先 Tobject简介
TObject是所有子类的祖先类。如果不指定父类的话,
Delphi便指定父类是 TObject。 TObject声明了一些方法,这
些方法可以被继承到子类中,有些还可以被覆盖。它的方法
分为以下三类,
(1)构造和析构方法。每个对象都有一个构造方法 Create
用来初始化对象。对象可以调用 NewInstance分配内存,然
后用将内存清 0。每个对象还有一个析构方法 Destroy,当对
象建立后,这个函数只被调用来释放对象。
(2)类方法。使用类方法 ClassInfo,ClassName,
ClassParent, ClassType, InheritsFrom及 InstanceSize,
可以得到关于该类或它的实例的有用信息。还可以得到块内
属性和方法的实时类型信息,FieldAddress,
MethodAddress及 MethodName。
(3)消息处理。使用 Dispatch和 DefaultHandler调用每个
对象的内置消息处理方法。
6.6 Delphi中的 VCL
Delphi中所有的构件继承于 TObject,TObject
是所有子类的祖先,它支持多态性,并且包
含构造方法 Create和析构方法 Destroy。
6.6.1 VCL的层次结构
VCL的层次结构如图 6-2所示。 (详情请见
P190)
6.6.2 自定义组件
Delphi的构件使用和构件制作采用同样的工作环境和相似的编程
方法,只要弄清基本原理,制作构件无需学习多少新东西。
制作构件的基本过程可以概括为,
⑴ 编写构件单元 ( unit) 。 这包含构件声明和构件实现代码 。
⑵ 按照与普通 Delphi单元同样的方法编译和调试构件单元 。
⑶ 创建构件注册单元 。 其中用 uses语句连接构件单元, 并用
register过程完成构件的注册 。
⑷ 编写构件联机帮助信息, 并编译成标准 Windows帮助文件 。
全部工作完成后,生成构件单元二进制文件(,dcu)、构件注
册源文件(,pas)和帮助信息文件(,hlp)及附加的关键词文
件(,kwf)。用户拿到这些文件后,就可以安装使用了。
1,确定一个父类
制作构件第一件事就是先择 Delphi对象类型作为父对象,以派生新的对象。
子对象可以继承父对象的全部非 Private部件,但不能摆脱不需要的部件。因此,
所选的对象应尽可能多的包含对象所需的属性、时间和方法,但不应包含子对
象不需要的东西。
2,添加属性
在构件中,属性和方法往往可以相互替代。对构件用户来说,属性比方法更
直观简便。因此,只要可能,应尽量以属性取代方法。属性类型包括简单类型、
枚举类型、集合类型、对象类型和数组类型。 其定义方法如下,
type
private
flayers,integer; { 内部存储用的变量 }
function getlayers,integer; { 用来读属性值的方法 }
procedure setlayers( alayers,integer) ; { 用来写属性值的方法 }
published
property layers:integer read getlayers write setlayers default 1;
end;
创建一个新组件的过程
3,事件与事件处理过程
创建构件时,事件也被当做属性来处理,区别仅在于事件
必须定义为过程类型,使其成为一个隐蔽指针,指向某个潜在
的过程。当构件用户为事件指定处理子程序后,事件便成为指
向该子程序的指针。 事件的定义方式如下,
type
private
Fonclick,Tnotifyevent; { 声明事件变量以保存过程指针 }
published
property Onclick, Tnotifyevent read Fonclick write
Fonclick;
end;
? 此例正是 Delphi标准组件中 Click事件的定义方式。
创建一个新组件的过程
4,方法处理
方法处理在创建构件时和使用构件时没有多大区别, 但有些
问题仍需要注意 。 首先要注意的是, 构件通常是在事件处理过
程中调用, 而构件作者又无法预测用户将在什么环境下如何调
用构件 。 因此, 构件中的方法应尽量避免占用系统资源, 避免
使 Windows停止对用户操作的反应 。
创建构件时应随时意识到, 此构件不仅可以直接调用, 而且
可用来创建别的构件 。 即使是对用户隐蔽的方法也应具有完整
的功能和清晰的接口 。 除了属性读写方法之外, 内部方法一般
应声明为 Protected虚方法, 以便被派生对象继承和重载 。 属性
读写方法则应采用 Private声明严密保护 。 派生对象如果需要读
写父对象的属性值, 应该访问属性本身, 没有必要直接访问其
读写方法 。
创建一个新组件的过程
5.构件测试
制作构件的核心工作是编写构件单元,构件单元的设计方式与一般
Delphi单元没有什么不同,只是构件单元 中不能包含窗体。
测试时,需先建立一个窗体单元,然后进行以下操作,
( 1)把被测构件单元名称加入窗体单元的 Uses语句中,并在 Public部
分声明被测构件的对象实例。
( 2)在窗体单元的 FormCreate子程序中调用被测构件的 Create方法,
以构造构件实例,其 Ownet参数设置为 Self,即窗体本身。然后给 Parent属
性赋值,并适当设置其他属性值。 Parent是容纳构件的父对象,如果是窗体
本身,应设置为 Self。
( 3)运行包含测试窗体的工程,找出构件程序中的错误。
创建一个新组件的过程
6,注册构件
注册构件用的程序代码可以放在构件单元中,但在 Delphi下
注册构件时要求提供包含注册代码的源程序文件,因此,比较
好的方式是把构件核心代码编译成,dcu文件或,dll动态链接库,
在注册源文件中只放注册代码和外围程序。
7,提供联机帮助
Delphi的帮助信息与 Windows一般帮助信息结构基本上相同,
其编写方法可参见有关资料。但 Delphi包含一个特殊的帮助搜索
引擎,能跨越多个帮助文件搜索关键词。因此,在构件帮助文
件中不仅要有普通 k型关键词脚注,还要包含 Delphi所用的 B型关
键词脚注。
创建一个新组件的过程
6.7 自定义类
在 Delphi中定义一个类要用到关键字 Class。
例如,下面的一小段代码就定义一个名字为 Employee的类,
Type
TEmployee= Class

End;
? 在对类命名时,微软推荐使用语言的命名规则。根据这
种命名规则,就意味着类名的第一个字母必须大写,并且
后面的并发连接词的第一个字母均为大写。
6.7 自定义类
一般自定义类的语法结构如下,
Type
TClassName = Class(TObject)
public
{public fields}
{public methods}
protected
{protected fields}
{protected methods}
private
{private fields}
{private methods}
end;
? 参见:例 6-6