http://www.cnblogs.com/keycode/archive/2010/10/15/1852462.html
Object
Passal的程序结构很特殊,与其它语言如C++,
Object
Windows等结构都不同。一个Delphi程序由多个称为单元的源代码模块组成。使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建在不同程序中使用的程序库。
Program
单元是一个特殊的单元,类似于C语言中的Main程序,即为应用程序的主程序。一个程序可以有多个单元组成,也可以只有一个
Program
单元组成,例如前面我们介绍过的DOS窗口程序就只有一个
Program
单元组成。下面是一个典型的
Program
单元:
program
Project1;
Application
.
CreateForm(TForm1, Form1);
标识符为单元的名字,各单元之间用逗好(,)隔开,最后结束用分号(;)。
注意:每个程序总是自动包含System单元,
Program
单元的
Uses
部分不能显式指定。System单元用于实现一些低级的运行时间程序的支持,如文件输入输出(I/O)、字符串操作、浮点运算、动态内存分配等。另外,Delphi在发行时提供了许多预先定义的单元,在构造程序时可以直接使用。例如,如果你将一个核对框放进一个窗体,你就自动地使用了StdCtrls单元,因为TCheckBox构件在StdCtrls中定义。
Uses
部分列出单元的顺序决定它们初始化的顺序,并影响编译器定位标识符的顺序。如果两个单元定义了一个相同名字的类型,编译器将总是使用前面那个单元的类型。
(
3
)程序块由保留字
Begin
和
End
括起来的一段代码组成,用于对程序的初始化。
UNIT
单元相当于C语言的子程序。基本上Delphi每个窗体都一个对应的单元。当你为应用程序创建窗体时,你就创建了一个与该窗体相联系的新单元。然而,单元也可以独立于窗体而存在。例如,一个单元可以只包含数学运算程序,而不需要有窗体。
一个单元可以由多个程序共享。单元的磁盘文件名后缀为.pas。
不管单元是否与窗体相关,单元的基本结构都是一样的 。
UNIT
单元由单元首部、接口部分(
interface
part)、实现部分(
implementation
part)、可选择的初始化部分(
initialization
part)、结束部分(
finalization
part)、
end
.组成。
单元的首部用保留字
Unit
开始,后跟单元名。单元名必须遵循标识符的所有一般原则(不能以数字开头等)。下面的单元名将是有效的:
在单元名之后是接口部分。接口部分用于声明变量、类型、过程和函数等。在接口部分声明的变量、类型以及过程、函数等是其它使用该单元的程序或单元等都可见的。接口部分用保留字
Interface
标明开始,用implemention标明结束。接口部分只能含有声明部分。
一个单元的接口部分还作为该单元说明文件的起点。虽然接口部分没有告诉你子程序做什么或变量如何使用,但它正确告诉了你的变量、类型、过程、函数等的名字及其调用方法。
接口部分本身又可以由几个可选的部分组成,分别是单元的
USES
语句、常量声明部分、类型声明部分、变量声明部分、过程和函数声明部分。其中常量声明、类型声明、变量声明、过程和函数声明部分用于声明其它使用该单元的单元可以访问的变量、类型、过程和函数等。
而
USES
语句列出该单元要用到的标准单元和其它单元,用于把外部的已定义的常量、类型、变量、过程或函数引入到本单元中使用。
USES
语句紧跟在
Interface
之后。
单元的第二部分,称为实现部分(
Implementation
),主要用于定义接口部分声明过的过程、函数等的代码。实现部分用保留字
implementation
标明,总是紧随接口部分之后。
实现部分也可以用
USES
语句列出该单元要用到的标准单元和其它单元等。如上面的
uses
MDIEdit;语句。实际上,实现部分也可以声明变量、数据类型、过程及函数等。
但是,在实现部分定义的变量、类型、过程、函数等只能由本单元自己使用(
private
declarations),使用该单元的其它单元或程序不可见的。私有定义可以隐藏单元的细节。
USES
子句用于访问其它单元。例如,如果你要让程序来效仿一个电传打字机,可以在
USES
包含WinCRT,因为WinCrt含有进行这个仿效所需要的程序。
Delphi提供了许多预先定义的单元,你可以在程序中直接使用。实际上,当你将一个新构件放入设计的窗体时,DElphi会自动将该构件的单元放入
USES
子句中。例如,如果你将Color Grid放入窗体,则单元ColorGrd就附加在窗体单元的
USES
子句末尾,从而ColorGRd单元中接口部分所有定义都是窗体单元可以访问的。
要使用
USES
子句包含单元中的程序,只要在单元名后加上程序名即可。例如,如果要在Unit2中访问Unit1中的ComputePayMent函数。
USES
子句可以放在接口部分(保留字
Interface
之后),也可放在实现部分(保留字
Implementation
之后),但是
USES
子句必须出现在它所有子程序、数据类型或变量被使用之前 。
USES
子句放在实现部分可以隐藏单元的内部细节,同时可以避免循环引用发生。
8.2.6
初始化部分(
Initialization
)
初始化部分是单元的可选部分,主要用于对单元数据的初始化,一般很少用到。
该部分总是在其它程序代码之前运行。如果一个程序包含多个单元,则在程序的其它部分运行之前,每个单元的初始化代码都会先调用,其顺序是单元显示在
Uses
语句的顺序。
完成部分(
Finalization
)是初始化过程的反过程,只要单元有初始化部分,才会有完成部分。完成部分对应在Delphi1
.0
中ExitProc和AddEXitProc函数。
完成部分在程序关闭时运行。任何在单元初始化时获得的资源如分配内存、打开文件等通常都在完成部分释放。单元完成部分的执行顺序与初始化部分相反。例如假如程序以A、B、C的顺序初始化单元,则完成过程的顺序是C、B、A。
一旦单元的初始化部分开始执行,就必须保证程序关闭时对应的完成部分执行。完成部分必须能够处理不完全初始的数据,因为如果产生异常,初始化代码可能不能完全执行。
构造用建立对象,并对对象进行初始化。通常,当调用构造时,构造类似一个函数,返回一个新分配的并初始化了的类类型实例。
构造跟一般的方法不同的是,一般的方法只能在对象实例中引用,而构造既可以由一个对象实例引用,也可以直接由类来引用。当用类来引用类的构造时,实际上程序做了以下工作:
(
2
)然后对这块区域缺省初始化。初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为
Nil
,字符串类型的字段清为空等。
(
4
)返回一个新分配的并初始化了的类类型实例。返回值的类型必须就是类的类型。
当你用在对象实例中引用类的构造时,构造类似一个普通的过程方法。这意味着一个新对象还没有被分配和初始化,调用构造不返回一个对象实例。相反,构造只对一个指定的对象实例操作,只执行用户在构造语句中指定的操作。
例如,在创建一个新的对象时,尽管还没有对象实例存在,仍然可以调用类的构造,程序示例如下:
TShape =
class
(TGraphicControl)
procedure
PenChanged(Sender: TObject);
procedure
BrushChanged(Sender: TObject);
constructor
Create(Owner: TComponent);
constructor
TShape
.
Create(Owner: TComponent);
FPen
.
OnChange := PenChanged;
FBrush
.
OnChange := BrushChanged;
构造的第一行是
Inherited
Create(Owner),其中
Inherited
是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为
0
(对于有序类型)、
nil
(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了
Inherited
Create(Owner)这句外,不需要写任何代码。
如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个
try
協inally语句,例如:
构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。
析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下:
destructor
TShape
.
Destroy;
上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。
析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。
前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为
Nil
。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush
.
Free和FPen
.
Free,Free方法的实现是:
if
Self <>
nil
then
Destroy;
也即Free方法在调用Destroy前会自动判断指针是否为
Nil
。如果改用FBrush
.
Destroy和FPen
.
Destroy,当这些指针为
Nil
时将产生异常导致程序中止。
声明方法的语法规则中,method directives为方法的指令字。
从语法示意图中可以看出,方法按指令字分又可分为三种,分别是虚拟、动态、消息方法,它们分别是方法名后用Virtual,Dynamic,Message保留字指定。也可以不加方法指令字,这种情况下声明的方法是静态的(static)。
另外,从语法示意图中可以看出,一个方法也可以像函数那样,指定参数的传递的方式,也即方法的调用约定。一个方法调用约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。
缺省情况,所有的方法都是静态的,除非你为方法提供了其它指令字。静态方法类似于通常的过程和函数,编译器在编译时就已指定了输出该方法的对象实例。静态方法的主要优点是调用的速度快。
当从一个类派生一个类时,静态方法不会改变。如果你定义一个包含静态方法的类,然后派生一个新类,则被派生的类在同一地址共享基类的静态方法,也即你不能重载静态方法。如果你在派生类定义一个与祖先类相同名的静态方法,派生类的静态方法只是替换祖先类的静态方法。例如:
type
TFirstComponent =
class
(TComponent)
procedure
Move;
procedure
Flash;
TSecondComponent =
class
(TFirstComponent)
function
Flash(HowOften:
Integer
):
Integer
;
上面代码中,第一个类定义了两个静态方法,第二个类定义了于祖先类同名的两个静态方法,第二个类的两个静态方法将替换第一个类的两个静态方法。
虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定的,而是程序在运行期根据调用这个虚拟方法的对象实例来决定的,这种方法又为滞后联编。 虚拟方法在对象虚拟方法表(VMT表)中占有一个索引号。
VMT表保存类类型的所有虚拟方法的地址。当你从一个类派生一个新类时,派生类创建它自己的VMT,该VMT包括了祖先类的VMT,同时加上自己定义的虚拟方法的地址虚拟方法可以在派生类中重新被定义,但祖先类中仍然可以被调用。例如:
type
TFirstComponent =
class
(TCustomControl)
procedure
Flash; virtual;
TSecondComponent =
class
(TFirstComponent)
procedure
Beep; override;
上例中,祖先类TFirstComponentw中方法Flash声明为虚拟的,派生类TSecondComponent重载了方法Flash。声明派生类的Flash 时,后面加了一个Override指令字,表示被声明的方法是重载基类中的同名的虚拟或动态方法。
注意:重载的方法必须与祖先类中被继承的方法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型一致。
要重载祖先类中的方法,必须使用Override批示字,如果不加这个指令字,而在派生类中声明了于祖先类同名的方法,则新声明的方法将隐藏被继承的方法。
所谓动态方法,非常类似于虚拟方法,当把一个基类中的某个方法声明为动态方法时,派生类可以重载它,如上例的Beep。不同的是,被声明为动态的方法不是放在类的虚拟方法表中,而是由编译器给它一个索引号(一般不直接用到这个索引),当调用动态方法时,由索引号决定调用方法的哪个来具体实现。
从功能上讲,虚拟方法和动态方法几乎完全相同,只不过虚拟方法在调用速度上较快,但类型对象占用空间大,而动态方法在调用速度上稍慢而对象占用空间小。如果一个方法经常需要调用,或该方法的执行时间要求短,则在虚拟和动态之间还是选择使用虚拟为好。
在方法定义时加上一个message指令字,就可以定义一个消息句柄方法。消息句柄方法主要用于响应并处理某个特定的事件。
TTextBox =
class
(TCustomControl)
procedure
WMChar(
var
Message: TWMChar); message WM_CHAR;
上例中声明了一个名叫TTextBox的类类型,其中还声明了一个过程WMPaint,只有一个变量参数Message,过程的首部后用保留字Message表示这是个消息句柄,后跟一个常量WM_PAINT表示消息句柄要响应的事件。
Object
Pascal规定消息句柄方法必须是一个过程,并且带有一个唯一的变量参数。message保留字后必须跟随一个范围在
1
到
49151
的整型常量,以指定消息的ID号。注意,当为一个VCL控制定义一个消息句柄方法时,整型常量必须是Windows的消息ID。(Delphi的Messages单元列出了所有Windows的消息ID。
注意:消息句柄不能使用Cdecl调用约定,也不能用Virtual,Dynamic,Override或Abstract等指令字。
在消息句柄中,你还可以调用缺省的消息句柄,例如上例中,你声明了一个处理WM_PAINT消息的方法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的方法名称不一样,也就是说你未必知道缺省句柄,那怎么调用呢?没关系,
Object
Pascal只要你使用一个保留字
Inherited
就可以了,例如:
procedure
TTextBox
.
WMChar(
var
Message: TWMChar); message WM_CHAR;
上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用
Inherited
保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。
使用
Inherited
保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。
从图
7.7
的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。
抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。
声明一个抽象方法是用Abstract指令字,例如:
procedure
Draw; virtual; abstract;
上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用
Inherited
保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。
在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。
规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。
为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如:
重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字
inherited
(继承)调用祖先类中相同的方法来实现。例如:
在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如:
constructor
Create(AOwner: TComponent); overload; override;
constructor
Create(AOwner: TComponent; Text:
string
); overload;
如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如:
procedure
Test(I:
Integer
); overload; virtual;
procedure
Test(S:
string
); reintroduce; overload;
SomeObject
.
Test(
'Hello!'
);
在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如:
function
Func(P:
Integer
):
Integer
;
function
Func(P:
Boolean
):
Integer
特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。
Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。
要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。
Object
Pascal使用保留字
Property
声明特性。
特性的声明由保留字
Property
,特性标识符,可选的特性接口(
Property
Interface
)和特性限定符(
Property
Specifier)构成。
特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。
在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和
Write
指定的方法去读或写字段的值。程序示例如下:
type
TYourComponent =
class
(TComponent)
procedure
SetCount (Value:
Integer
);
property
Count:
Integer
read FCount
write
SetCount;
上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是
Integer
,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。
特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个
Integer
型变量AInteger,程序可以这么写:
实际上,编译器根据声明中的Read子句和
Write
子句自动把上述语句分别转换成:
Ainteger:=Aobject
.
Fcount;
顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用
With
语句则可简化。
跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。
特性限定符可以有四类,分别是Read,
Write
,Stored和Default。其中Read和
Write
限定符用于指定访问特性的方法或字段。
注意:Read和
Write
限定符指定的方法或字段只能在类的
Private
部分声明,也就是说它们是私有的(关于
Private
的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。
Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。
从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。
Write
限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“
Set
”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。
在
Write
限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。
从语法上讲,可以没有
Write
限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。
Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。
Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为
False
时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为
True
,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。
TSampleComponent =
class
(TComponent)
function
StoreIt:
Boolean
;
property
Important:
Integer
stored
True
;
property
Unimportant:
Integer
stored
False
;
property
Sometimes:
Integer
stored StoreIt;
上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。
Default限定符用于指定特性的缺省值,在Delphi的
Object
Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是
False
,Down特性的缺省值是
False
,这些缺省值都是通过Default限定符设定的,程序示例如下 :
TStatusBar =
class
(TPanel)
constructor
Create(AOwner: TComponent); override;
property
Align default alBottom;
constructor
TStatusBar
.
Create(AOwner: TComponent);
inherited
Create(AOwner);
上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。
注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。
如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。
所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下:
TDemoComponent =
class
(TComponent)
function
GetNumberName(Index:
Integer
):
string
;
property
NumberName[Index:
Integer
]:
string
read GetNumberName;
function
TDemoComponent
.
GetNumberName(Index:
Integer
):
string
;
-MaxInt..-
1
: Result :=
'Negative'
;
1..100
: Result :=
'Small'
;
101..
MaxInt: Result :=
'Large'
;
上例中,声明了一个数组特性NumberName,它的元素类型是
String
,索引变量是Index,索引变量的类型是
Integer
。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。
对于数组特性来说,可以使用Read和
Write
限定符,但Read和
Write
限定符只能指定方法而不能是字段,并且
Object
Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。
Write
限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。
访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。
所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。
这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在
Protected
部分声明,现在把它移到
Published
部分声明。
特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例:
property
Size:
Integer
read FSize;
property
Text:
string
read GetText
write
SetText;
property
Color: TColor read FColor
write
SetColor stored
False
;
type
TDerived =
class
(TBase)
property
Size
write
SetSize;
published
property
Text;
property
Color stored
True
default clBlue;
对于祖先类中的Size特性,增加了
Write
限定符,对于祖先类中的Text特性,改在
Published
部分声明,对于祖先类中的Color特性,首先是改在
Published
部分声明,其次是改变了Stored限定符中的表达式,从
False
改为
True
,并且增加了一个Default限定符。
面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在
Object
Pascal中,是通过这么几个保留字来设置成员的可见性的:
Published
,
Public
,
Protected
,
Private
,Automated。如
procedure
SetMinValue(Value:
Longint
);
procedure
SetMaxValue(Value:
Longint
);
function
GetPercentDone:
Longint
;
procedure
Paint; override;
constructor
Create(AOwner: TComponent); override;
procedure
AddProgress(Value:
Longint
);
property
PercentDone:
Longint
read GetPercentDone;
property
MinValue:
Longint
read FMinValue
write
SetMinValue default
0
;
property
MaxValue:
Longint
read FMaxValue
write
SetMaxValue default
100
;
property
Progress:
Longint
read FCurValue
write
SetProgress
上例中,FMinValue、FMaxValue、FCurValue等字段是在
Private
部分声明的,表示它们是私有的,
Public
部分声明的几个方法是公共的。
procedure
SetMinValue(Value:
Longint
);
procedure
SetMaxValue(Value:
Longint
);
function
GetPercentDone:
Longint
;
procedure
Paint; override;
constructor
Create(AOwner: TComponent); override;
procedure
AddProgress(Value:
Longint
);
property
PercentDone:
Longint
read GetPercentDone;
property
MinValue:
Longint
read FMinValue
write
SetMinValue default
0
;
property
MaxValue:
Longint
read FMaxValue
write
SetMaxValue default
100
;
property
Progress:
Longint
read FCurValue
write
SetProgress;
上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{
$M
+}状态编译或者继承的是用{
$M
+}状态编译的基类,其可见性为为
Published
,否则就是
Public
。
在
Private
部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,
Private
部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。
在
Public
声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的
Public
部分声明,然后把Unit1加到Init2的
Interface
部分就可以了。
注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的
Public
部分,以防止程序意外地不正确地修改了数据。
在
Published
部分声明的成员,其可见性与在
Public
部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,
Published
和
Public
的区别在于成员的运行期类型信息不同。一个
Published
元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有
Published
接口,该接口被一些Delphi工具使用,例如
Object
Inspector。
注意:只有当编译开关
$N
的状态为
$M
+时或者基类是用
$M
+编译时,类的声明中才能有
Published
部分,换句话说,编译开关
$M
用于控制运行期类型信息的生成。
Protected
与
Private
有些类似。在
Protected
部分声明的成员是私有的(受保护的),不同的是在
Protected
部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。
在
Protected
部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。
C++的程序员可能对这个保留字比较陌生,在Automated部分声明的成员类似于在
Public
部分声明的成员,它们都是公共的,唯一的区别在于在Automated部分声明的方法和特性将生成OLE自动化操作的类型信息。
注意:Automated只适用于基类是TAuto0bject的类声明中,在Automated部分声明的方法,其参数和返回类型(如果是函数的话)必须是可自动操作的。在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可自动操作的,否则将导致错误。可自动操作的类型包括:
Byte
、
Currency
、
Double
、
Integer
、
Single
、
SmallInt
、
String
、TDateTime、Variant、WordBool等。
在Automated部分声明的方法只能采用Register调用约定,方法可以是虚拟的但不能是动态的。在Automated部分声明的特性只能带Read和
Write
限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和
Write
指定的只能是方法而不能是字段,方法也只能采用Register调用约定,也不允许对特性重载。
在Automated部分声明的方法或特性分配一个识别号(ID),如果不带DispId限定符,编译器自动给方法或特性分配一个相异的Id,如果带DispId限定符,注意Id不能重复。
一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码:
T3DScreenThing =
Class
(TScreenThing)
procedure
ResetScreenThing(T:TScreenThing);
procedure
Reset3DScreenThing(T:T3DScreenThing);
Q:TScreenThing; R:T3DScreenThin;
在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。
我们介绍过的Delphi的VCL构件都是使用类类型定义的对象。在Delphi中,所有的类都是从一个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了一些操纵类的最基本的方法,是Delphi所有类的缺省祖先类。使用View|Browse命令,可以打开Browse
Object
命令,查看Delphi各对象之间的继承关系。
TObject类是一切构件类和对象的基类,位于继承关系的最顶层。TPersistent类是TObject类的下一级继承者,它是一个抽象类,主要为它的继承者提供对流的读写能力。
TComponent类是TPersistent类的下一级继承者,它是VCL中所有构件的祖先类。TComponent类定义了构件最基本的特性、方法和事件。尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有几个非可视的构件,如TTime构件和TDataSource构件等,绝大多数构件是从TComponent类的下级TControl类继承下来的,从TControl类继承下来的都是可视化的构件,这些构件也称为控制。TControl类定义了VCL中所有可视化构件基本的特性、方法和事件等。
TWinControl和TGraphicControl类都是TControl类的子类。TWinControl的子类主要是用于窗口控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗口,有自己的句柄,占用Windows资源,并且可以与用户交互。而TGraphicControl的子类没有窗口句柄,也不占用Windows资源类,也能接受键盘的输入,它们的主要优点在于节约资源,如TLabel和TSpeedButton等构件。
实际上,整数类型可以分为基本整数类型(Fundamental
type
)和一般整数类型(generic
type
)。一般整数类型(generic
type
)包括
Integer
和
Cardinal
两种。在实际编程时,请尽量区分这两种,因为底层CPU和操作系统对结果进行了优化。
表
6
-
2
列出了
Integer
和
Cardinal
的取值范围及存储格式。
Integer
?
147483648..2147483647
32
位有符号
Cardina l0
..4294967295
32
位无符号
基本整数类型包括
Shortint
、
Smallint
、
Longint
、
Int64
、
Byte
、
Word
和
Longword
。表
6
-
3
列出了它们的取值范围及存储格式。
Shortint
-
128..127
signed
8
-bit
Smallint
-
12768..32767
signed
16
-bit
Longint
-
2147483648..2147483647
signed
32
-bit
Int64
-
2
^
63..2
^
63
? signed
64
-bit
Byte
0..255
unsigned
8
-bit
Word
0..65535
unsigned
16
-bit
Longword
0..4294967295
unsigned
32
-bit
一般整数类型的实际范围和存储格式随着
Object
Pascal的不同实现而不同,但通常根据当前CPU和操作系统来采取最佳的操作方式。
一般整数类型是最常用的整数类型,可以充分利用CPU和操作系统的特性,因此程序中应当尽量使用一般整数类型。基本整数类型独立于操作系统和CPU,只有当应用程序的数据范围或存储格式特别需要时,才使用基本整数类型。
通常情况下,整数的算术运算结果为
Integer
类型,等价于
32
位的
Longint
类型。只有当操作数存在
Int64
类型时,才返回
Int64
类型的值。因此,下面的代码将产生错误的结果:
var
I:
Integer
; J:
Int64
;
在这种情况下,要取得一个
Int64
的值,必须进行类型转换:
注意:绝大多数例程在遇到
Int64
时都把它转换为
32
位。但例程High,Low,Succ,Pred,Inc,Dec,IntToStr和IntToHex则完全支持
Int64
参数。Round,Trunc,StrToInt64,和StrToInt64Def函数可以返回
Int64
类型的结果。
字符类型中
Char
类型设计来只存储一个字符。一个字符占一个字节,因此
Char
数据类型可以存储
256
个不同的字符,其对应的整数为
0
到
255
。
除了
Char
数据类型外,Delphi还提供了
Char
类型的扩展,即
AnsiChar
和
WideChar
型。表
6
-
4
是字符数据类型的列表。
WideChar
2
存储一个UniCode字符。
Char
1
目前,对应
AnsiChar
。但Delphi将来的版本可能对应于
WideChar
。
Ansi字符集是扩展的ASCII字符集,仍然占一个字节。目前,
Char
对应
AnsiChar
,但Borland公司在将来的Delphi版本中可能使
Char
对应
WideChar
。
WideChar
用来支持泛字符集(Unicode)。Unicode字符占用两个字节,可以有
65536
种不同的取值,可以表达现代计算机中使用的世界上所有的字符,包括图形符号和用于出版业的特殊符号等。
UniCode字符集的前
256
个字符对应着ANSI字符。如果你把一个
AnsiChar
字符放到
WideChar
字符类型的变量中,
WideChar
字符类型变量的高字节将全部置为
0
,
AnsiChar
字符存放到
WideChar
字符类型的变量的低字节中。
注意:Windows NT全面支持Unicode字符号集,但Windows
95
却不同。如果你希望书写的程序同时能在两种系统上运行,必须使用SizeOf()函数,以确定字符占多少字节。
Boolean
数据类型的变量只存储一个逻辑值,例如
True
或
False
。共有
4
种
Boolean
数据类型,见表
6
-
5
。
Delphi提供多种
Boolean
数据类型的目的是为了兼容,因为在某些情况下,Windows需要用一个字(
2
个字节)或双字(
4
个字节)来表示一个布尔值。
所谓枚举类型,就是用一组数量有限的标识符来表示一组连续的整数常数,在类型定义时就列出该类型可能具有的值。枚举类型是一种用户自定义的简单数据类型。在类型定义时就列出该类型可能具有的值。下面是枚举类型定义的一些例子:
type
TDays=(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday);
TPrimaryColor=(Red,Yelloow,Blue); TDepartment=(Finance,Personnel,Engineering,Marketing,MIS); TDog=(Poodle,GoldenRetriever,Dachshund,NorwegianElkhound,Beagle);
枚举类型定义中的每个值都对应一个整数,整数值由该值在类型定义表中的位置决定,通常类型定义的第一个数对应的整数值为
0
。例如,在TDay类型定义中Monday对应值为
0
、Tuesday值为
1
,等等。如果你把DayOfWeek定义为
Integer
,通过赋整数值来代表星期几,也可以得到同样的结果。但是,由于枚举类型表达的意思明确、直观、便于记忆,因此使用枚举类型仍有必要。
其中标识符列表中的标识符之间用逗号隔开,它列出该类型可能具有的值。
var
DayOfWeek:(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday);
1
)枚举的元素只能是标识符,标识符的命名必须符合 Pascal关于标识符的规定,例如下面的声明就是错误的:
type
TDays=(
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
);
2
)同一个枚举元素不能出现在多个枚举类型中。例如下面的声明就是错误的:
type
TColors1=(Red,Blue,Green,White,Black);
TColors2=(Yellow,Lime,Silver,Green);
3
)不能直接用枚举类型中的元素参加运算,例如,下面的语句就是错误的:
但是,可以用某个枚举类型中的元素对枚举变量赋值,例如,下面的语句:
子界类型是
Integer
,
Boolean
,
Char
及枚举型等称为宿主类型数据的一个子集。当你要限制一个变量的数据范围时,使用子界类型就特别有用。子界类型也是一种用户自定义的简单数据类型。要定义子界类型,必须说明区间的最大值和最小值,下面是子界类型定义的一些例子:
type
TCompassRange =
0..360
;
TMonthlyIncome=
10000..30000
;
THours =
0..23
; TPrecipitation=(Drizzle,Showers,Rain,Downpour,Thunderstorm);
TRain =Drizzle
..
Downpour;
其中两个常数(称为上界和下界)必须是同一种有序类型,如
Integer
,
Boolean
,
Char
及枚举型等,但不能是
Real
数据类型。第一个常数必须小于或等于第二个常数。
var
Compass:TCompassRange;
1
)上界常数和下界常数必须是同一类型,且都是有序类型。
2
)子界类型变量具有宿主类型数据的所有运算特性,但运算的结果必须在范围内。
type
Color = (Red, Green, Blue);
实数类型是带有小数部分的数值,存储实数。有
6
种不同的
Real
数据类型,它们在范围、精确度、大小等方面都不相同。见表
6
-
6
。
Real48
2.9
x
10
^-
39
..
1.7
x
10
^
38
11..12
6
Single
1.5
x
10
^-
35
..
3.4
x
10
^
38
7..8
4
Double
5.0
x
10
^-
324
..
1.7
x
10
^
308
15..16
8
Extended
3.6
x
10
^-
4951
..
1.1
x
10
^
4932
19..20
10
Comp
-
2
^
63
+
1
..
2
^
63
?
19..20
8
Currency
22337203685477.5808..922337203685477.5807
19..20
8
Delphi在处理字符串时,提供了多种方式,表
6
-
7
是Delphi使用的字符串类型。
ShortString
255
个字符
AnsiChar
否
AnsiString
~
2
^
31
个字符
AnsiChar
是
String
或者
255
或者~
2
^
31
个字符
ANSIChar
都可能
WideString
~
2
^
30
个字符
WideChar
是
从上表可知,Delphi主要支持两种类型的字符串:
ShortString
和
AnsiString
。
WideString
类似于
AnsiString
,只是其存储的字符为
WideChar
。
ShortString
数据类型定义的是长度在
1
到
255
之间动态长度字符串。像数组一样,单个字符可以通过引用它们的索引进行存取。位于
0
的字节存储了代表字符串当前所赋值长度的数值(只能通过关闭范围检查才能访问)。
ShortString
数据类型主要是为了能和Delphi
1.0
和Borland Pascal的早期版本兼容。
AnsiString
(又称为long
String
或huge
String
)数据类型的定义是动态分配的,长度几乎可以无限的(仅受可用内存限制)以NULL结尾的字符串。
AnsiString
中的字符由
AnsiChar
数据类型的字符组成。
建议最好使用
AnsiString
数据类型。这是因为
AnsiString
数据类型的变量是动态分配的,当把一个更长的字符串放入
AnsiString
数据类型的变量时,Delphi会从新为该变量申请空间。如果要显式地改变字符串的长度,可以使用SetLength() 函数来分配一块恰当的内存;使用
AnsiString
数据类型的另外一个优点是,
AnsiString
字符串是以NULL结尾,即在最后一个字符之后自动加上一个NULL字符表示字符串结束,与操作系统的大多数函数例程兼容,例如Win32 API,从而在调用操作系统函数例程时更加方便,不需要使用StrPCopy()来将以Pascal风格的字符串转换为以NULL结尾的字符串。Delphi VCL构件的所有特性、事件使用
AnsiString
来传递参数,以简化、统一VCL和API之间的接口。
String
既可以是
SHortString
类型也可以是
AnsiString
类型,缺省是
AnsiString
类型。例如,如果你像下面那样定义字符串:
则编译器假定你要创建一个
AnsiString
数据类型变量。
使用
$H
编译命令可以改变缺省定义。当在程序中把编译开关
$H
的状态改为
时,
String
缺省是
ShortString
类型;当在程序中把编译开关
$H
的状态改为
时,
String
缺省是
AnsiString
类型。例如:
如果定义中指明了长度(最大为
25
5
),则
String
为
ShortString
。例如:
结构类型在内存中存储一组相关的数据项,而不是像简单数据类型那样单一的数值。
Object
Pascal结构类型包括集合类型、数组类型、记录类型、文件类型、类类型、类引用类型、接口类型等。这里,我们只介绍集合类型、数组类型、记录类型和文件类型。类类型、类引用类型和接口类型放在下一章介绍。
数组是一种数据类型数据的有序集合,是代表一定数量具有相同类型变量的一种数据类型。
Object
Pascal数组可与任何简单数据类型或字符串类型等一起使用。数组可用于声明一个简单变量或作为一个记录类型定义的组成部分。
要声明一个数组变量,要求你提供一个标识符,使用
array
保留词,在方括号中指定数组的界限,并指定编译器数组将用于存储什么类型,例如:
Var
Check:
array
[
1..100
]
of
Integer
;
范围标点‘..’用于表示Check是一个有
100
个整数的数组,这些整数从
1
到
100
编号。范围编号是一个子界类型,可以是
0
,也可以是正数或负数,或者字符,或其它有序类型。
Type
TCheck =
array
[
1..100
]
of
Integer
;
Var
CheckingAccount:TCheck;
上面是先定义数组类型,然后定义数组变量。其实上,也可以同时定义类型、变量,例如:
var
Kelvin:
array
[
0..1000
]
of
Temperatures;
TwentiethCentury:
array
[
1901..2000
]
of
Events;
LessThanzeroo:
array
[-
999
..-
400
]
of
Shortint
;
DigiTValues:
array
[
'0'
..
'9'
of
Byte
;
SecretCode:
array
[
''
A
'..'
Z'
of
char
;
访问数组中的元素很简单,只要在标识符后面的方括号中给出指定的元素的索引号即可。例如:
要访问数组中的所有元素,可以使用循环语句。例如 :
For
J:=
1
to
10
do
Check[J]:=
0
;
上面介绍的是一维数组。实际上,数组可以是多维的。例如,如果你想编写一个数组来容纳一张电子表格中的值,那么就可以使用
2
维数组。下面的例子说明如何使用
2
维数组定义一个有
20
行和
20
列的表格:
Type
Ttable =
array
[
1..20
,
1..20
]
of
Double
;
要将
2
维数组中的所有数据初始化,可以使用如下语句:
使用多维数组时,要记住的一件事是数组为每维所占据的RAM数都呈幂级数增加。例如:
Aline:
Array
[
1..10
]
of
byte
;占用
10
个字节
AnArea:
Array
[
1..10
,
1..10
]
of
byte
;占用
10
*
10
=
100
个字节
Avloume:
Array
[
1..10
,
1..10
,
1..10
]
of
byte
;占用
10
*
10
*
10
=
1000
个字节
前面介绍的字符串,实际上就是一个
1
维字符数组,只不过Pascal对字符串类型作了特殊的准许,你可以把它看作一个整体。字符串类型本质上等同于下列类型:
type
StringType:
array
[
0..255
]
of
char
;但是,虽然你可以把一个字符串看待,但它仍然保持其数组的特性。例如在定义一个字符串类型变量时,你可以说明字符串的大小,就像你定义字符数组的大小一样。下面是几个字符串类型定义:
type
MyString:
string
[
15
];
上面语句定义MyString类型包含
15
个字符,LittleString包含
1
个字符,BigString没有说明大小,就取字符串包含字符的最大个数
255
。然后你可以定义这些类型的变量,就像使用其它类型一样:
Letter,Digit:LittleString;
你可以对字符串变量进行赋值: MyName:=
'Frank P.BorLand'
;
?因为MyName长度为
15
,因此只能容纳
15
个字符。如果执行下面语句: MyName:=Frank P
.
Borland?则MyName变量中只存有FranK
.
P
.
Borlan其余部分被舍弃。
为了取得字符串中的一个字符,可以按如下方法进行:AChar:=MyName[
2
];
但是,如果索引大于字符串变量的长度,则结果不可知。例如: AChar:=MyName[
16
];则AChar将被设置为某个不确定的字符,换句话说,就是废字符。
在字符串类型的数组中,字符串的第一个位置[
0
]包含有字符串的长度,因此数组的实际长度比该字符串长度大
1
个字节。你可以使用Length函数或下面的代码来得到一个字符串的长度:L:=Ord(
String
[
0
]);
数组类型常量的每个字段都是类型常量,下面是声明数组类型常量的语法规则(图
6.5
)。
从图中可以知道,一个数组类型常量由括号括起来的类型常量组成,不同类型常量用逗号隔开。
像简单类型常量一样,数组类型常量用来定义一个数组常量,下面是一个例子。
type
TStatus = (Active, Passive, Waiting);
TStatusMap =
array
[TStatus]
of
string
;
const
StatStr: TStatusMap = (
'Active'
,
'Passive'
,
'Waiting'
);
上面的例子首先定义一个数组TStatusMAp,然后定义一个数组常量StatStr。该数组常量的目的是把TStatus类型的值转化为对应的字符串。下面是数组常量StatStr元素的值:
StatStr[Active] =
'Active'
StatStr[Passive] =
'Passive'
StatStr[Waiting] =
'Waiting'
数组常量的元素类型可以是除文件类型以外的任何类型。字符数组类型常量既可以是字符也可以是字符串,例如:
const
Digits:
array
[
0..9
]
of
Char
= (
'0'
,
'1'
,
'2'
,
'3'
,
'4'
,
'5'
,
'6'
,
'7'
,
'8'
,
'9'
);
该数组常量也可以表示为:
const
Digits:
array
[
0..9
]
of
Char
=
'0123456789'
;
初始化字符数组类型常量的字符串长度可以小于数组类型的定义长度,例如:
var
FileName:
array
[
0..79
]
of
Char
=
'TEST.PAS'
;这时数组余下的字符空间自定置NULL(#
0
),因此数组也变成了一个以NULL结尾的字符串。
多维数组类型常量的定义采用括号的形式,每一维用括号括起,不同维及不同元素常量之间用逗号隔开。最里面的常量对应最右面的维数。
例如:
type
TCube =
array
[
0..1
,
0..1
,
0..1
]
of
Integer
;
const
Maze: TCube = (((
0
,
1
), (
2
,
3
)), ((
4
,
5
), (
6
,
7
)));
所谓开放式数组,是指数组作为形参传递给过程或函数时其长度是可变的,这样在调用过程或函数时,可以传递不同长度的数组作为实际参数。
开放式数组在过程或函数中作为形参可以定义为:
array
of
T这里T是数组的元素类型标识符,实际参数必须是T类型的变量或元素类型为T的数组变量。在过程或函数内形参的作用可看作为下面的数组:
array
[
0..
N -
1
]
of
T
这里N是实参中元素的个数。实际上实参的上下界被映射到
0
到 N-
1
。如果实参是类型T的简单变量,则它被看成为只有类型T元素的数组。
开放数组只能以开放数组参数或一个未定义变量参数的的形式传递到过程或函数。开放数组可以作为数值参数、常数参数或变量参数,并与这些参数具有同样的语法规则。作为形式参数的开放数组不允许整体赋值,只能访问它的元素。并且对元素的赋值不影响实参。
当开放式数组作为数值参数时,编译器将在内存中开辟一块区域存放实参的拷贝,等过程或函数退出后再释放这块区域,这样当实参是个很大的数组时,可能会发生栈溢出的问题。在使用开放数组参数时,可以使用Low函数获得当前最小下标(不过总是为
0
),使用High函数获得当前最大下标,使用SizeOF函数获得当前数组大小。下面是一个例子,演示了开放式数组的使用。
Var
X1:
array
[
1..10
]
of
Double
;
X2:
array
[
1..30
]
of
Double
;
procedure
Clear(
var
A:
array
of
Double
);
function
Sum(
const
A:
array
of
Double
):
Double
;
var
I:
Integer
; S:
Double
;
for
I :=
0
to
High(A)
do
S := S + A[I];
当开放式数组的元素类型为
Char
时,实参可以是一个字符串常数。例如:
procedure
PrintStr(
const
S:
array
of
Char
);
if
S[I] <> #
0
then
Write
(S[I])
在Delphi中,除了定义静态数组外,还可以定义动态数组。动态数组只需说明数组的类型信息(包括数组的维数和数组元数的类型),但不需要定义元素的个数。例如:
A:
array
[
1..100
]
of
string
;
C:
array
of
array
of
string
;
这里A是静态数组,B是一维的整数动态数组,C是二维的字符串动态数组。
动态数组没有固定的长度。相反,当为动态数组赋值或使用SetLength过程时,动态数组的内存空间将重新分配。动态数组的定义形式是:
例如:
var
MyFlexibleArray:
array
of
Real
;
定义了一个类型为实数型的一维动态数组。注意,声明语句并没有为MyFlexibleArray分配内存。要为动态数组分配内存,需要调用SetLength过程。例如:
SetLength(MyFlexibleArray,
20
);上面语句分配
20
个实数,标号从
0
到
19
。
动态数组的标号是整数类型,标号总是从
0
开始。使用Length,High和Low函数可以取得有关动态数组的特性。Length函数返回数组中元素的个数。High函数返回数组的最大标号,Low返回
0
。
集合类型是
Integer
,
Boolean
,
Char
,枚举型,子界型等类型数据的一个子集。在应用程序中,当要检测一个数是否属于一个特定的集合时,就可以使用集合类型。(
1
)集合类型的定义下面是声明一个集合类型的语法规则(图
6.6
)。
其中
Set
of
是保留字,ordinal
Type
是集合的基类型,可以是任何有序类型如整数型,布尔型,字符型,枚举型和子界型,但不能是实型或其它自定义类型。下面是一些集合类型的例子:
type
VoterDataSet=
Set
Of
(Democrat,Republican,Male,Female, LowOpinion,HighOption,Confused);
Letters =
Set
of
'a'
..
'z'
;
VIBGYOR= (Violet,Indigo,Blue,Green,Yellow,Orange,Red);
ColorSet =
set
of
VOBGYOR;
一个集合类型的变量的值实际上是它的基类型的一个子集,可以为空集。一个集合最多可有
256
个元素。因此下面的集合定义是错误的:
type
SET1=
Set
Of
Integer
;
这是因为
Integer
集合的元素个数远远大于
256
。
[Democrat];表示只含Democrat的集合。
一个集合可以拥有
0
个元素,这时称之为空集,用两个方括号表示,其中什么也没有。对于集合类型变量,你可以进行+,-,=,*(并),
IN
等运算。见下表
6
-
8
。
+ 往一个集合中添加元素 Aset:=Aset+AnotherSet;
- 从一个集合中去除元素 Aset:=Aset-AnotherSet;
* 去除两个集合中都没有的元素 Aset:=Aset*AnotherSet;
In
测试元素 Bool:=AnElement
in
Aset
Voter:=Voter-[HighOption];
If
HighOption
in
Voter
then
SendtheVoterFlowers;
常量像简单类型常量一样,集合类型常量用来定义一组常量的集合。例如:
type
TDigits =
set
of
0..9
;
TLetters =
set
of
'A'
..
'Z'
;
const
EvenDigits: TDigits = [
0
,
2
,
4
,
6
,
8
];
Vowels: TLetters = [
'A'
,
'E'
,
'I'
,
'O'
,
'U'
,
'Y'
];
上面的例子首先定义两个集合类型TDigits和Tletters,然后定义了两个集合常量,其中EvenDigits的值域是[
0
,
2
,
4
,
6
,
8
],它为TDigits的一个子集;Vowels的值域是 [
'A'
,
'E'
,
'I'
,
'O'
,
'U'
,
'Y'
],它为TLetters的一个子集。
记录是一系列相关的变量,这些变量被称为域,它们放在一起,作为一个整体使用。例如,一个雇员可能包含姓名、雇用时间、薪金等数据,这时你可以像下面那样定义一个雇员记录类型:
type
TEmployee =
record
LastName:
String
[
20
];
Pascal的记录类型跟数据库中的记录很相似,记录类型中的元素可以理解为数据库中的字段,事实上Pascal正是借用了数据库中的记录和字段的概念。
记录可以一个字段也没有,即为空记录;一个记录可以有一个固定部分(fixed part),在固定部分,每个字段都有其确定的标识符和数据类型,它们在内存中分别占用不同的区域;一个记录也可以加入一个可变部分(variantpart)。声明记录变量与声明其它类型变量一样,下面是两个记录变量的说明:
var
NewEmployee,PromotedEmployee:TEmployee;
记录类型中的每个域都有一种数据类型,你既可以单独访问这些域,也可以把记录作为一个整体来使用。例如,你可以像下面那样访问NewEmployee记录中的Salary域或整个记录:
NewEmployee
.
Salary:=
43211.00
;
PromotedEmployee:=NewEmployee;
当你要访问记录内的域时,需要指定记录名,并在记录名后加(.),然后跟上域名。例如:
PromotedEmployee
.
Position
如果要对多个域赋值,则每个域前都必须加记录名。例如:
PromotedEmployee
.
LastName :=
'Gates'
PromotedEmployee
.
FirstName:=
'Bill'
PromotedEmployee
.
YearHired:=
1990
;
PromotedEmployee
.
Salary:=
92339.00
;
PromotedEmployee
.
Position:=
'Manager'
?Pascal提供了
With
语句,使你可以减少重复书写记录名的烦恼。
With
语句的语法是:
With
记录变量名
Do
...
每个可变部分由至少由一个常量(Constant)标识,所有常量必须是唯一的,并且类型为与tag field
type
指定类型相容的类型。Identifier用于记录可变部分的可选部分,称为识别字段标识符。如果定义了识别字段标识符,程序可以使用该标识符决定在给定的时间内哪个可变部分是活动的,如果没有定义识别字段标识符,程序必须根据其它规则选定记录可变部分。
type
TPolygon =
record
X, Y:
Real
;
TRectangle: (Height, Width:
Real
);
TTriangle: (Side1, Side2, Angle:
Real
);
注意:记录可变部分的字段不能是长字符串类型和变体类型,也不能含有长字符串类型和变体类型分量的构造类型。
一个记录类型常量每个字段由一个标识符和类型常量组成,不同字段用分号隔开,字段部分用括号括起。像简单类型常量一样,记录类型常量用来定义一个记录常量,下面是一些例子。
type
TMonth = (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
SomeDay: TDate = (D:
2
; M: Dec; Y:
1960
);
上面的例子首先定义一个记录类型TDate,然后定义了一个记录常量SomeDay。注意:记录类型常量中个字段的出现顺序必须与记录类型定义中的顺序一致。如果记录类型包括文件类型字段,则不能定义该记录的记录常量。如果记录类型包括可变部分,则只有被选择的可变部分可以定义常量。
文件是指相同类型元素的有序集合。Delphi处理文件有三种方式,一种是使用
Object
Pascal标准的文件处理技术;一种是使用Windows的文件处理函数;还有一种是使用文件流对象。
Object
Pascal标准的文件处理技术,有专门的数据类型和文件处理例程,并且与Windows的标准API不兼容,但对于熟悉Pascal的用户来说,仍然是操作文件的好选择。下面我们就对此进行介绍。
声明一个文件类型的语法如下:
type
fileTypeName =
file
of
type
这里,fileTypeName是任何有效的标识符,
type
是一种大小固定的数据类型,称之为基类型。 基类型不能使用指针类型,不能包括动态数组、长字符串、类、对象、指针、可变类型以及其它文件类型。但可以是其它结构类型,例如:
PhoneEntry =
record
FirstName, LastName:
string
[
20
];
PhoneList =
file
of
PhoneEntry;
这里,PhoneEntry是自定义的结构类型,PhoneList是以PhoneEntry为基类型的文件类型。在定义了文件类型后,就可以直接定义文件类型的变量了。例如:
有时侯,我们也可以使用
file
of
基类型的方式直接定义文件类型变量。例如下面的定义与上面的形式有同样的效果:
var
List1:
file
of
PhoneEntry;
如果我们在声明文件类型时不指明其基类型。则这样的文件我们称之为无类型文件,如:
无类型文件主要用于直接访问磁盘文件的多层I/O操作。
6.3.4
指针类型指针类型对程序员来说可能是最复杂和最灵活的数据类型。当你在Delphi中创建一个数据结构时,首先要分配内存空间。分配的内存空间用于存储数据结构中的数据。而指针就是指向分配空间的内存地址。使用指针,可以使程序不必每次需要时都去分配,只要申请一次即可,其它过程或函数使用同一块内存空间时,只要使用该内存空间的地址。例如,假设你的一个邻居问你怎样去百货店,你并不需要把整个百货店搬到邻居家里,只需要告诉它去百货店的路径即可,这个路径类似于一个指针。
其中基类型可以是简单类型,也可以是前面介绍的结构,或只是一个标识符。如果基类型是一个未定义的类型标识符的话,则该类型标识符必须在同一块内声明。
PersonType=
Record
LAstNAme:
String
;
FirstNAme:
String
; Age:
Integer
;
PersonPointer = ^PersonType;
上例中,声明了三个指针类型,一个是WordPtr,指向^
Word
,一个是RealPtr,指向^
Real
,还有一个是PersonPointer,指向一个标识符,而该标识符标识一个记录类型。
Delphi提供专门的过程和函数操作指针,这些过程和函数是:New过程,@操作符,PTR函数,GetMem过程。下面分别介绍。
New过程是 Pascal中的标准例程(在System单元声明),用于在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量。New过程的语法为:
procedure
New(
var
P:
Pointer
);
其中P是一个指针变量。所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。
新分配的内存空间由P指向,P^即为类型的动态变量。应用程序不再需要该动态变量时,可以调用Dispose标准例程释放为该变量分配的内存空间。
type
PListEntry = ^TListEntry;
P^.Text :=
'Hello world'
;
上例中,声明了一个指针类型 PListEntry,指向标识符 TListEntry,而该标识符标识一个记录类型 TlistEntry,然后定义了两个指针变量List和P。程序首先用New过程在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量P。P^即为记录类型TListEntry的动态变量。不再需要该动态变量后时,调用Dispose释放为该变量分配的内存空间。
@操作符是个一元操作符,用于获得操作数的地址,其使用语法见图
6.11
。
从图中可以知道,@后面的操作数可以是变量、过程、函数或类类型中的方法。
procedure
ChangeValue(X:
Integer
)
上例中,ChangeVAlue过程首先声明了一个指向整型数的指针Ptr,然后用@操作符取出X的地址赋予IntPtr指针,并显示Ptr指针指向的数,最后改变这个数。
Ptr函数是 Pascal中的标准例程(在System单元声明),用于把一个指定的地址转换为指针。Ptr函数的语法为:
function
Ptr(Address:
Integer
):
Pointer
;
其中Address是一个整数,用于表示一个
32
位地址,函数执行的结果是把
32
位地址转化为指针。
GetMem过程也是Pascal中的标准例程(在System单元声明),类似于New,用于在应用程序堆栈中为动态变量申请一块指定大小的区域,并把该区域的地址赋予指针变量。GetMem函数的语法为:
procedure
GetMem(
var
P:
Pointer
; Size:
Integer
);
所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。如果程序不再需要该动态变量时,可以调用FreeMem标准例程释放为该变量分配的内存空间。
AssignFile(F,
'test.txt'
);
BlockRead(F, Buffer^, Size);
ProcessFile(Buffer, Size);
上例打开一个名字为test
.
txt的文件,并把文件读入动态分配的缓冲区,缓冲区大小为文件的大小,然后对文件进行处理,最后释放动态分配的缓冲区,并关闭文件。
Pascal中有一个特殊的保留字
nil
,这是一个空指针常量,当指针的值为
nil
时,表示指针当前没有指向任何动态变量。值为
nil
的指针变量不能访问动态变量。
无类型的指针是指指针变量在声明时没有指明基类型。无类型指针在声明中只使用
Pointer
。例如:
指针pAnyPoint可以指向任何变量类型。无类型的指针的作用是它可以指向任何类型,但是,不能用指针变量符后加^的形式来引用它的动态变量。
字符指针类型即
PChar
数据类型,是一个指向以NULL(不是零)字符结尾的字符(
Char
)串的指针。这种类型主要用于与外部函数如在Windows API中所用的函数兼容。与Pascal字符串不同,Windows和C字符串没有一个长度字节。取而代之的是它们从
0
字节索引开始,以一个NULL(#
0
)结束。Pascal RTL字符函数根据长度决定存储在字符串变量中的字符数目。C函数实际上一次搜索字符数组的一个字符,直到碰到NULL,表示字符串结尾。在Windows API中所用的许多函数以指向NULL结束字符串或用NULL结束填入缓冲区的字符。在Pascal中使用这些函数就需要
PChar
类型变量。内存将分配给变量并被所需函数使用。
除了
PChar
外,Delphi还包含
PAnsiChar
和
PWideChar
数据类型。
PAnsiChar
数据类型是一个指向以NULL(不是零)字符结尾的
AnsiChar
字符串的指针,在Delphi中,
PCHAR
等同于
PAnsiChar
。
?
PWideChar
数据类型是一个指向以NULL(不是零)字符结尾的
WideChar
字符串的指针,用于UniCode字符集。实际上,
PAnsiChar
和
PWideChar
数据类型的定义为:
字符串类型与
PCHAR
类型赋值兼容,即一个字符串可以直接赋给一个
PCHAR
类型的变量,例如:
上面赋值语句首先申请一块区域,该区域包含字符串
'Hello world...'
,并在最后加上NULL,然后P指向这块内存区。上述例子等价于下列形式:
const
TempString:
array
[
0..14
]
of
Char
=
'Hello world...'
#
0
;
Object
Pascal允许把过程和函数作为一个整体赋给变量和作为参数传递。实现这一功能的途径是使用
Object
Pascal的过程类型。
声明一个过程类型的语法与声明过程或函数的首部的语法相似,不同的是声明一个过程类型时不需要过程或函数保留字后面的标识符。声明过程类型时可以指定一种调用约定方式,缺省的调用方式是Register。下面是声明过程类型的举例:
TStrProc =
procedure
(
const
S:
string
);
TMathFunc =
function
(X:
Double
):
Double
;
上例声明的三个过程类型中,第一个是不带任何参数的过程,第二个是带一个参数S的过程,第三个是带一个参数X的函数,函数返回值为
Double
。
过程类型根据其是否运用于对象分为两类:全局过程指针和方法指针。
声明过程类型时不带
of
Object
的是全局过程指针。全局过程指针指向的是全局的过程或函数。例如上面的过程类型Tprocedure,TstrProc,TMathFunc都是全局过程指针。
声明过程类型时带有
of
Object
的是方法指针。方法指针指向的是一个对象的过程或函数方法。例如下面的过程类型是方法指针。
type
TMethod =
procedure
of
object
;
TNotifyEvent =
procedure
(Sender: TObject)
of
object
;
声明过程类型变量的方法与声明其它类型变量的方法相同,下面例子声明两个过程类型变量:
一个全局过程或函数标识符一个方法指示符下面举例说明过程类型的用法。
type
TMainForm =
class
(TForm)
procedure
ButtonClick(Sender: TObject);
function
Tan(Angle:
Double
):
Double
;
Result := Sin(Angle) / Cos(Angle);
上例的TMainForm是一个类类型,TMathFunc是前面定义的全局过程指针,TnotifyEvent是前面定义的方法指针。其中MathFunc和OnClick是两个过程类型变量。变量MathFunc和OnClick的赋值方式为:
MathFunc := Tan;OnClick := MainForm
.
ButtonClick;
过程类型变量值等于
NIL
表示该过程类型变量没有赋值,因此在过程语句或函数调用中使用值等于
NIL
的过程类型变量将发生错误。防止的办法是使用Assigned()函数。例如:
if
Assigned(OnClick)
then
OnClick(Self);
如果给定的过程类型变量已经赋值,Assigned函数返回
TRUE
,如果给定的过程类型变量值为
NIL
,Assigned函数返回
FALSE
。在把一个过程或函数赋给一个过程类型变量时要注意赋值兼容,必须满足下列条件:调用约定方式必须相同。?参数个数必须相同,相应的数据类型必须相同。?函数返回的值类型必须相同。
Variant主要用于表达需要动态改变类型的数据。例如,当一个数据的实际类型在编译时不知道或运行时需要改变类型时,就可以使用Variant类型。
Variant类型变量可以包含
integer
,
real
,
string
,
boolean
, 日期和时间等类型值或以及 OLE自动化对象等,还可以表示长度和维数可变的数组。
Variant变量在首次创建时,总是被初始化为Unassigned。Unassigned是Variant变量的一个特殊值,表明Variant变量还未赋值,Variant变量的另一个特殊值是NULL,指示Variant变量未知或丢失数据。
Object
Pascal是一种类型严谨的程序设计语言,不是所有类型的数据都可以互相赋值的。只有赋值两边的数据类型一致或兼容才可以进行赋值操作。下面就有关数据类型兼容和强制数据类型转换等概念进行介绍。
所闻类型兼容,是指一种类型的数据可以与另一种类型的数据进行关系运算。类型兼容是赋值兼容的前提条件,也是
Object
Pascal数据运算的基本前提。
?
Object
Pascal规定,只有满足下列条件才是类型兼容:
?两种类型都是紧凑字符串类型,并具有相同的元素个数。
?一种类型是字符串类型,另一种类型是字符串类型、紧凑字符串类型或字符类型。
?一种类型是
Pointer
类型,另一种类型是任意的指针类型。
?两种类型都是类类型或,类引用类型,并且一种类型继承了另一种类型。
?一种类型是
PChar
类型,另一种类型是形式为
array
[
0..
X]
of
Char
的字符数组。
?两种类型都是基类型相同的指针类型(编译开关
$T
设置为{
$T
+})。
?两种类型都是结果类型相同、参数个数相同、参数类型一致的过程类型。
?一种类型是Variant类型,另一种类型是整型、实型、字符串类型或布尔类型。
当两个类型要进行关系运算操作而又不满足类型兼容时,将产生编译错误。
类型兼容仅仅可以进行关系运算,只有赋值兼容的变量才可以赋值或进行参数传递。
类型T2的值与类型T1的值赋值兼容是指T1和T2允许赋值操作,即:
Object
Pascal规定,类型T1的值与类型T2的值赋值兼容必须有满足下列条件:
?T1和 T2类型相同,并且都不是文件类型或包含文件类型的自定义类型。
?T1是T2是兼容的有序类型,类型T2的值在类型类型T1的取值范围内。
?T1和 T2都是实型, 类型T2的值在类型T1的取值范围内。
?T1和T2是兼容的、集合类型。 类型T2的所有成员在类型T1的取值范围内。
?T1是
PChar
类型,T2是形式为
array
[
0..
X]
of
Char
的字符数组。
?T1是过程类型,T2是具有与T1嗤峁嘈拖嗤⒉问鍪嗤⒉问嘈鸵恢碌墓袒蚝?br> ?T1是Variant类型,T2是
Integer
,
real
,
string
或
boolean
类型。
?T1是
Integer
,
real
,
string
或
boolean
类型,T2是Variant类型。当两个类型要进行赋值操作而又不满足赋值兼容时,将产生编译错误。
变量强制类型转换就是强制将一种类型变量转换为另一种类型的变量。程序员自己确定强制类型转换的合法性。
当变量强制类型转换应用于一个变量时,该变量就被视为由类型标识符说明的类型。变量的大小必须与类型标识符说明的类型的大小相同。变量之前可以放置一个或多个类型允许的限定符。
Word
类型的变量W转换为TByteRec, TWordRec(L)将一个
LongInt
类型的变量L转换为TWordRec类型,而PByte(L)则将
LongInt
类型变量L转换为指针类型Pbyte。
数值强制类型转换就是强制将数值(或表达式)从一种类型转换为另一种类型。
其中表达式类型必须是有序类型或指针类型。 在转换中,如果结果类型的大小不同于表达式类型的大小,则有可能造成数据的截止或扩展。下面举例说明数值强制类型转换的用法。
mod
运算符的结果是两个操作数相除后的余数的整数部分。
*, /,
div
,
mod
,
and
,
shl
,
shr
,
as
2
乘法运算符
=, <>, <, >, <=, >=,
in
,
is
4
(最低) 关系运算符
这些例子都是DOS窗口方式的,而不是通常的Windows应用程序。如果读者要调试这些程序,需要修改Delphi的一些缺省设置。其步骤是:
(
2
)如果当前不是自动打开一个新项目 ,选择
File
|New命令开始一个新项目 。
(
3
)选择Project|Options|Linker命令,使能Generate Console Application核对框,从而是Delphi创建的程序是DOS窗口方式的,而不是通常的Windows应用程序。
(
4
)选择View|Project Source命令,进入代码编辑器编辑项目 文件代码,键入本书提供的例子。
(
5
)运行程序,程序将在它自己的DOS窗口运行。要关闭DOS窗口,选择Alt+F4或单击窗口的右上角单击X。
ASM
块语句在Pascal中嵌入汇编语言代码。由于Delphi Pascal对计算机资源提供了很好的支持,因此,除非特别需要,一般不需要使用汇编语句。
Object
Pascal使用控制语句来控制程序的执行顺序。
7
个是分支语句,
3
个是循环语句:
ELSE
Writeln
(
'Bad Choice,Try Again.'
);
请注意,
CASE
语句的常量范围不能重叠。
Else
要放在所有判断语句之后.
GOTO
语句强行将程序转向一个指定的点执行。该指定点用一个标号标识。
EXIT语句的功能是退出当前的代码块。如果代码块是主程序,EXIT语句导致程序的终止;如果当前块是嵌套的,EXIT语句跳到外一层嵌套继续执行。如果当前块是过程或函数,EXIT语句导致过程或函数执行终止,跳到调用过程或函数的语句的下一条语句执行。
HALT语句导致程序的非正常结束,并返回到操作系统。通常是在程序遇到致命错误时才使用HALT语句。HALT语句后可跟一个整数代码HALT(
1
),以指定错误的原因。
WHILE
key<>
$D
DO
key:=GetChar;
for
V := Expr1
to
Expr2
do
Body; 可使用
DOWNTO
;在循环体中,如果不想执行循环下面的语句,而直接进入下一次循环,可以使用Continue语句.
要退出循环,跳到
FOR
/
DO
语句下面的语句执行,可以使用break语句;如果不想执行循环下面的语句,而要求直接进入下一个循环,可以使Continue语句。
要定义和调用一个过程,首先要在程序的
TYPE
区声明一个过程.
procedure
NumString(N:
Integer
;
var
S:
string
);
过程声明之后,就应当在
Implementation
区定义这个过程,定义的规则如下。过程:
Procedure
<name>(<Parameters>)
函数的定义和调用与过程的定义和调用类似,不同的是函数的首部,函数的首部多了一个返回结果类型。
function
Max(A: Vector; N:
Integer
):
Extended
;
Max函数返回类型为
Extended
。函数声明之后,就应当在
Implementation
区定义这个函数。
(
2
)返回值送给Delphi的一个内置变量Result。
如果你写的函数有可能移植到其它Pascal编译器中使用,最好使用第一种方式。
从前面说明的过程和函数的语法规则我们知道,在声明过程或函数时,可以在附属块指定过程或函数的参数的传递方式。Pascal共提供了五种传递方式,分别为Register,Pascal,Cdecl,Stdcall,SafeCall。缺省的调用方式是Register方式。如果一个过程或函数没有指定过程或函数的调用方式,就采用Register调用方式。
function
Max(A: Vector; N:
Integer
):
Extended
;Stdcall;
Object
Pascal调用方式的区别于以下几点:
Register和 Pascal调用方式传递参数是从左到右,即最左边的参数先产生并首先传递,最右边的参数最后产生并最后传递。而Cdecl, Stdcall和 Safecall 调用方式传递参数则是从右到左。
使用Pascal、Stdcall和Safecall调用方式的过程或函数在返回时程序自动删除堆栈中的参数,而Cdecl调用方式必须在程序返回时调用者自己删除堆栈中的参数。
Register调用方式使用三个CPU寄存器来传递参数,而其它调用方式使用堆栈来传递参数。
Register调用方式通常是最快的参数传递方式,因为它不需要创建栈帧。Pascal和 Cdecl调用方式通常用于调用用C,C++或其它语言书写的动态链接库程序。Stdcall 调用方式通常用于Windows API程序。而Safecall调用方式通常用于实现OLE自动化编程的双接口(Dual interfaces)。
在声明过程或函数时,可以在附属块使用指示字以进一步指定过程或函数的产生方式。Delphi过程或函数分别提供了Block,External,
Asm
,Forward。指定调用方式的语法示例如下:
procedure
MoveWord(
var
Source, Dest; Count:
Integer
); external;
其中Block是缺省方式,表示过程或函数的语句部分是 Pascal程序快,下面对External,Assembler,Forward进行介绍。
该指示字表示过程或函数是外部的,通常用于从动态链接库中引入过程或函数。External后可以动态链接库名或表示动态链接库的有序数,也可以指定引入的过程或函数名。例如:
function
MessageBox(HWnd:
Integer
; Text, Caption:
PChar
; Flags:
Integer
):
Integer
; stdcall; external
'user32.dll'
name
'MessageBoxA'
;
上例中,user32
.
dll指定用于引入过程或函数的动态链接库名(也可以是一个有序数),MessageBox指定从动态链接库中引入过程或函数名。
该指示字表示过程或函数是使用嵌入式汇编语言编写的。例如函数声明:
function
LongMul(X, Y:
Integer
):
Longint
;Assembler
function
LongMul(X, Y:
Integer
):
Longint
;
该指示字表示一个过程或函数是向前查找的。在声明了一个过程或函数是向前查找的之后,该过程或函数的定义必须在后面的某个地方定义。
procedure
Walter(M, N:
Integer
); forward;
procedure
Clara(X, Y:
Real
);
注意:不能在单元的
interface
部分声明向前查找过程。在使用向前查找过程时,要注意相互递归。
当调用过程或函数时,常常需要使用参数传递数据给被调用的过程或函数。在某种程度上,使过程、函数更有用更灵活的方法就是使用参数。
在Pascal中,调用过程或函数使用的参数称为实参,被调用过程或函数使用的参数称为形参,例如,下面语句中,Edit1是实参:
Procedure
ColorIt(AnEditBox:Tedit);
变量和结构被完整地拷贝到堆栈中,而不是通过机器的寄存器。通过值传递参数可以防止调用的函数修改原来的参数,因为调用的函数接收到的只不过是参数的一个副本。例如:
procedure
Tform1
.
Button1Click(Sender:Tobject);
Number:=StrToInt(Edit1
.
text);
Edit2
.
Text:=IntToStr(Number);
Procedure
Calculate(CalcNo:
Integer
);
在Calculate过程中,CalcNo参数按值传递,执行该过程后,CalcNo的值扩大了十倍。但是,调用过程Tform1
.
Button1Click中Number并没有改变,因此Edit1编辑框与Edit2编辑框的值一样。
(
2
)传引用(Passing By Reference)。
传递一个指向参数的引用(指针),按规则引用可用作指针和值。改变引用传递的参数要影响调用源参数的拷贝。
使用传引用必须在参数前加上
Var
保留字。例如,把Calculate改写如下:
procedure
Calculate(
Var
CalcNo:
Integer
);
修改后,Calculate过程的CalcNo参数为按引用传递,执行该过程后,CalcNo的值扩大了十倍,同时,调用过程Tform1
.
Button1Click中Number也作了改变,因此Edit2编辑框的值是Edit1编辑框值的
10
倍。
(
3
)常量传递(Constant Parameters)。
如果过程或函数运行时,形参的值永远都不会改变,就可以考虑使用常数参数。要使一个参数为常数参数,只要在参数前加上
Const
保留字。例如:
function
TDirectoryOutline
.
ForceCase(
const
AString:
string
):
string
;
if
Assigned(FCaseFunction)
then
Result := FCaseFunction(AString)
当你不需要参数改变时,可以使用常数参数防止偶然对该参数的修改。如果程序某个地方对常数参数进行了修改,你将会得到一个非法变量引用错误信息。
在Delphi中,可以为过程和函数定义默认参数。默认参数仅仅显示在参数列表的尾部,其形式是:
当调用包括默认参数的过程或函数时,默认参数的值可以省去。例如下面是一个函数的定义:
procedure
FillArray(A:
array
of
Integer
; Value:
Integer
=
0
);