1 Object Passal的程序结构很特殊,与其它语言如C++,Object Windows等结构都不同。一个Delphi程序由多个称为单元的源代码模块组成。使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建在不同程序中使用的程序库。 2 8.1 Program单元 3 Program单元就是Delphi中的项目文件。 4 Program单元是一个特殊的单元,类似于C语言中的Main程序,即为应用程序的主程序。一个程序可以有多个单元组成,也可以只有一个Program单元组成,例如前面我们介绍过的DOS窗口程序就只有一个Program单元组成。下面是一个典型的Program单元:program Project1; 5 uses Forms, 6 Unit1 in 'Unit1.pas' {Form1}; 7 {$R *.RES} 8 begin 9 Application.Initialize; 10 Application.CreateForm(TForm1, Form1); 11 Application.Run; 12 end 13 (1)程序首部指定程序名、以及参数等。 14 (2)Uses语句定义程序用到的所有单元。 15 标识符为单元的名字,各单元之间用逗好(,)隔开,最后结束用分号(;)。 16 注意:每个程序总是自动包含System单元,Program单元的Uses部分不能显式指定。System单元用于实现一些低级的运行时间程序的支持,如文件输入输出(I/O)、字符串操作、浮点运算、动态内存分配等。另外,Delphi在发行时提供了许多预先定义的单元,在构造程序时可以直接使用。例如,如果你将一个核对框放进一个窗体,你就自动地使用了StdCtrls单元,因为TCheckBox构件在StdCtrls中定义。 17 Uses部分列出单元的顺序决定它们初始化的顺序,并影响编译器定位标识符的顺序。如果两个单元定义了一个相同名字的类型,编译器将总是使用前面那个单元的类型。 18 (3)程序块由保留字Begin和End括起来的一段代码组成,用于对程序的初始化。 19 8.2 UNIT单元 20 UNIT单元相当于C语言的子程序。基本上Delphi每个窗体都一个对应的单元。当你为应用程序创建窗体时,你就创建了一个与该窗体相联系的新单元。然而,单元也可以独立于窗体而存在。例如,一个单元可以只包含数学运算程序,而不需要有窗体。 21 一个单元可以由多个程序共享。单元的磁盘文件名后缀为.pas。 22 8.2.1 单元结构 23 不管单元是否与窗体相关,单元的基本结构都是一样的 。UNIT单元由单元首部、接口部分(interface part)、实现部分(implementation part)、可选择的初始化部分(initialization part)、结束部分(finalization part)、end.组成。 24 8.2.2 单元首部 25 单元的首部用保留字Unit开始,后跟单元名。单元名必须遵循标识符的所有一般原则(不能以数字开头等)。下面的单元名将是有效的: 26 Unit Rsgl; 27 8.2.3 接口部分(Interface) 28 在单元名之后是接口部分。接口部分用于声明变量、类型、过程和函数等。在接口部分声明的变量、类型以及过程、函数等是其它使用该单元的程序或单元等都可见的。接口部分用保留字Interface标明开始,用implemention标明结束。接口部分只能含有声明部分。 29 一个单元的接口部分还作为该单元说明文件的起点。虽然接口部分没有告诉你子程序做什么或变量如何使用,但它正确告诉了你的变量、类型、过程、函数等的名字及其调用方法。 30 接口部分本身又可以由几个可选的部分组成,分别是单元的USES语句、常量声明部分、类型声明部分、变量声明部分、过程和函数声明部分。其中常量声明、类型声明、变量声明、过程和函数声明部分用于声明其它使用该单元的单元可以访问的变量、类型、过程和函数等。 31 而USES语句列出该单元要用到的标准单元和其它单元,用于把外部的已定义的常量、类型、变量、过程或函数引入到本单元中使用。USES语句紧跟在Interface之后。 32 8.2.4 实现部分 33 单元的第二部分,称为实现部分(Implementation),主要用于定义接口部分声明过的过程、函数等的代码。实现部分用保留字implementation标明,总是紧随接口部分之后。 34 实现部分也可以用USES语句列出该单元要用到的标准单元和其它单元等。如上面的uses MDIEdit;语句。实际上,实现部分也可以声明变量、数据类型、过程及函数等。 35 但是,在实现部分定义的变量、类型、过程、函数等只能由本单元自己使用(private declarations),使用该单元的其它单元或程序不可见的。私有定义可以隐藏单元的细节。 36 8.2.5 USES子句 37 USES子句用于访问其它单元。例如,如果你要让程序来效仿一个电传打字机,可以在USES包含WinCRT,因为WinCrt含有进行这个仿效所需要的程序。 38 USES WinCRT; 39 Delphi提供了许多预先定义的单元,你可以在程序中直接使用。实际上,当你将一个新构件放入设计的窗体时,DElphi会自动将该构件的单元放入USES子句中。例如,如果你将Color Grid放入窗体,则单元ColorGrd就附加在窗体单元的USES子句末尾,从而ColorGRd单元中接口部分所有定义都是窗体单元可以访问的。 40 要使用USES子句包含单元中的程序,只要在单元名后加上程序名即可。例如,如果要在Unit2中访问Unit1中的ComputePayMent函数。 41 USES子句可以放在接口部分(保留字Interface之后),也可放在实现部分(保留字Implementation之后),但是USES子句必须出现在它所有子程序、数据类型或变量被使用之前 。 42 USES子句放在实现部分可以隐藏单元的内部细节,同时可以避免循环引用发生。 43 8.2.6 初始化部分(Initialization) 44 初始化部分是单元的可选部分,主要用于对单元数据的初始化,一般很少用到。 45 该部分总是在其它程序代码之前运行。如果一个程序包含多个单元,则在程序的其它部分运行之前,每个单元的初始化代码都会先调用,其顺序是单元显示在Uses语句的顺序。 46 8.2.7 完成部分(Finalization) 47 完成部分(Finalization)是初始化过程的反过程,只要单元有初始化部分,才会有完成部分。完成部分对应在Delphi1.0中ExitProc和AddEXitProc函数。 48 完成部分在程序关闭时运行。任何在单元初始化时获得的资源如分配内存、打开文件等通常都在完成部分释放。单元完成部分的执行顺序与初始化部分相反。例如假如程序以A、B、C的顺序初始化单元,则完成过程的顺序是C、B、A。 49 一旦单元的初始化部分开始执行,就必须保证程序关闭时对应的完成部分执行。完成部分必须能够处理不完全初始的数据,因为如果产生异常,初始化代码可能不能完全执行。 50 1.构造 51 构造用建立对象,并对对象进行初始化。通常,当调用构造时,构造类似一个函数,返回一个新分配的并初始化了的类类型实例。 52 构造跟一般的方法不同的是,一般的方法只能在对象实例中引用,而构造既可以由一个对象实例引用,也可以直接由类来引用。当用类来引用类的构造时,实际上程序做了以下工作: 53 (1)首先在堆中开辟一块区域用于存贮对象。 54 (2)然后对这块区域缺省初始化。初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为Nil,字符串类型的字段清为空等。 55 (3)执行构造中用户指定的动作。 56 (4)返回一个新分配的并初始化了的类类型实例。返回值的类型必须就是类的类型。 57 当你用在对象实例中引用类的构造时,构造类似一个普通的过程方法。这意味着一个新对象还没有被分配和初始化,调用构造不返回一个对象实例。相反,构造只对一个指定的对象实例操作,只执行用户在构造语句中指定的操作。 58 例如,在创建一个新的对象时,尽管还没有对象实例存在,仍然可以调用类的构造,程序示例如下: 59 type 60 TShape = class(TGraphicControl) 61 private 62 FPen: TPen; 63 FBrush: TBrush; 64 procedure PenChanged(Sender: TObject); 65 procedure BrushChanged(Sender: TObject); 66 public 67 constructor Create(Owner: TComponent); 68 override; 69 destructor Destroy; 70 override; ... 71 end; 72 constructor TShape.Create(Owner: TComponent); 73 begin 74 inherited Create(Owner);{ Initialize inherited parts } 75 Width := 65;{ Change inherited properties } 76 Height := 65; 77 FPen := TPen.Create;{ Initialize new fields } 78 FPen.OnChange := PenChanged; 79 FBrush := TBrush.Create; 80 FBrush.OnChange := BrushChanged; 81 end; 82 构造的第一行是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为0(对于有序类型)、nil(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了Inherited Create(Owner)这句外,不需要写任何代码。 83 如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个try協inally语句,例如: 84 try 85 ...{ User defined actions } 86 except{ On any exception } 87 Destroy;{ Destroy unfinished object } 88 raise;{ Re-raise exception } 89 end; 90 构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。 91 2.析构 92 析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下: 93 destructor TShape.Destroy; 94 begin 95 FBrush.Free; 96 FPen.Free; 97 inherited Destroy; 98 end; 99 上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。 100 析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。 101 前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为Nil。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush.Free和FPen.Free,Free方法的实现是: 102 procedure TObject.Free; 103 begin 104 if Self <> nil then Destroy; 105 end; 106 也即Free方法在调用Destroy前会自动判断指针是否为Nil。如果改用FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产生异常导致程序中止。 107 7.2.3 方法指令字 108 声明方法的语法规则中,method directives为方法的指令字。 109 从语法示意图中可以看出,方法按指令字分又可分为三种,分别是虚拟、动态、消息方法,它们分别是方法名后用Virtual,Dynamic,Message保留字指定。也可以不加方法指令字,这种情况下声明的方法是静态的(static)。 110 另外,从语法示意图中可以看出,一个方法也可以像函数那样,指定参数的传递的方式,也即方法的调用约定。一个方法调用约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。 111 1.静态方法 112 缺省情况,所有的方法都是静态的,除非你为方法提供了其它指令字。静态方法类似于通常的过程和函数,编译器在编译时就已指定了输出该方法的对象实例。静态方法的主要优点是调用的速度快。 113 当从一个类派生一个类时,静态方法不会改变。如果你定义一个包含静态方法的类,然后派生一个新类,则被派生的类在同一地址共享基类的静态方法,也即你不能重载静态方法。如果你在派生类定义一个与祖先类相同名的静态方法,派生类的静态方法只是替换祖先类的静态方法。例如: 114 type TFirstComponent = class(TComponent) 115 procedure Move; procedure Flash; 116 end; 117 TSecondComponent = class(TFirstComponent) 118 procedure Move;{该MOVE不同于祖先类的MOVE} 119 function Flash(HowOften: Integer): Integer;{该Flash不同于祖先类的Flash} 120 end; 121 上面代码中,第一个类定义了两个静态方法,第二个类定义了于祖先类同名的两个静态方法,第二个类的两个静态方法将替换第一个类的两个静态方法。 122 2.虚拟方法 123 虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定的,而是程序在运行期根据调用这个虚拟方法的对象实例来决定的,这种方法又为滞后联编。 虚拟方法在对象虚拟方法表(VMT表)中占有一个索引号。 124 VMT表保存类类型的所有虚拟方法的地址。当你从一个类派生一个新类时,派生类创建它自己的VMT,该VMT包括了祖先类的VMT,同时加上自己定义的虚拟方法的地址虚拟方法可以在派生类中重新被定义,但祖先类中仍然可以被调用。例如: 125 type TFirstComponent = class(TCustomControl) 126 procedure Move;{ static method } 127 procedure Flash; virtual;{ virtual method } 128 procedure Beep; dynamic;{ dynamic virtual method } 129 end; 130 TSecondComponent = class(TFirstComponent) 131 procedure Move;{ declares new method } 132 procedure Flash; 133 override;{ overrides inherited method } 134 procedure Beep; override;{ overrides inherited method } 135 end; 136 上例中,祖先类TFirstComponentw中方法Flash声明为虚拟的,派生类TSecondComponent重载了方法Flash。声明派生类的Flash 时,后面加了一个Override指令字,表示被声明的方法是重载基类中的同名的虚拟或动态方法。 137 注意:重载的方法必须与祖先类中被继承的方法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型一致。 138 要重载祖先类中的方法,必须使用Override批示字,如果不加这个指令字,而在派生类中声明了于祖先类同名的方法,则新声明的方法将隐藏被继承的方法。 139 3.动态方法 140 所谓动态方法,非常类似于虚拟方法,当把一个基类中的某个方法声明为动态方法时,派生类可以重载它,如上例的Beep。不同的是,被声明为动态的方法不是放在类的虚拟方法表中,而是由编译器给它一个索引号(一般不直接用到这个索引),当调用动态方法时,由索引号决定调用方法的哪个来具体实现。 141 从功能上讲,虚拟方法和动态方法几乎完全相同,只不过虚拟方法在调用速度上较快,但类型对象占用空间大,而动态方法在调用速度上稍慢而对象占用空间小。如果一个方法经常需要调用,或该方法的执行时间要求短,则在虚拟和动态之间还是选择使用虚拟为好。 142 4.消息句柄方法 143 在方法定义时加上一个message指令字,就可以定义一个消息句柄方法。消息句柄方法主要用于响应并处理某个特定的事件。 144 把一个方法声明为消息句柄的示例如下: 145 type 146 TTextBox = class(TCustomControl) 147 private 148 procedure WMChar(var Message: TWMChar); message WM_CHAR; 149 ... 150 end; 151 上例中声明了一个名叫TTextBox的类类型,其中还声明了一个过程WMPaint,只有一个变量参数Message,过程的首部后用保留字Message表示这是个消息句柄,后跟一个常量WM_PAINT表示消息句柄要响应的事件。 152 Object Pascal规定消息句柄方法必须是一个过程,并且带有一个唯一的变量参数。message保留字后必须跟随一个范围在1到49151的整型常量,以指定消息的ID号。注意,当为一个VCL控制定义一个消息句柄方法时,整型常量必须是Windows的消息ID。(Delphi的Messages单元列出了所有Windows的消息ID。 153 注意:消息句柄不能使用Cdecl调用约定,也不能用Virtual,Dynamic,Override或Abstract等指令字。 154 在消息句柄中,你还可以调用缺省的消息句柄,例如上例中,你声明了一个处理WM_PAINT消息的方法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的方法名称不一样,也就是说你未必知道缺省句柄,那怎么调用呢?没关系,Object Pascal只要你使用一个保留字Inherited就可以了,例如: 155 procedure TTextBox.WMChar(var Message: TWMChar); message WM_CHAR; 156 begin 157 Inherited 158 ... 159 end; 160 上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用Inherited保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。 161 使用Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。 162 7.2.4 抽象方法 163 从图7.7的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。 164 抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。 165 声明一个抽象方法是用Abstract指令字,例如: 166 type 167 TFigure = class 168 procedure Draw; virtual; abstract; 169 ... 170 end; 171 上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用Inherited保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。 172 7.2.5 重载方法与重定义方法 173 在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。 174 规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。 175 为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如: 176 type 177 AClass=Class 178 procedure One;virtual; 179 procedure Two;{static method} 180 end; 181 BClass=Clas(Aclass) 182 procedure One;override; 183 procedure Two; 184 end; 185 重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字inherited(继承)调用祖先类中相同的方法来实现。例如: 186 procedure Bclass.One; 187 begin //new code 188 ...? 189 //call inherited procedure Bclass 190 inherited One; 191 end; 192 在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如: 193 constructor Create(AOwner: TComponent); overload; override; 194 constructor Create(AOwner: TComponent; Text: string); overload; 195 如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如: 196 type 197 T1 = class(TObject) 198 procedure Test(I: Integer); overload; virtual; 199 end; 200 T2 = class(T1) 201 procedure Test(S: string); reintroduce; overload; 202 end; 203 ... 204 SomeObject := T2.Create; 205 SomeObject.Test('Hello!'); // calls T2.Test 206 SomeObject.Test(7); // calls T1.Test 207 在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如: 208 type 209 TSomeClass = class 210 published 211 function Func(P: Integer): Integer; 212 function Func(P: Boolean): Integer // error 213 ... 214 7.3 类 的 特 性 215 特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。 216 Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。 217 7.3.1 声明特性 218 要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。Object Pascal使用保留字Property声明特性。 219 特性的声明由保留字Property,特性标识符,可选的特性接口(Property Interface)和特性限定符(Property Specifier)构成。 220 特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。 221 在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和Write指定的方法去读或写字段的值。程序示例如下: 222 type TYourComponent = class(TComponent) 223 private 224 FCount: Integer; { used for internal storage } 225 procedure SetCount (Value: Integer); { write method } 226 public 227 property Count: Integer read FCount write SetCount; 228 end; 229 上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是Integer,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。 230 特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个Integer型变量AInteger,程序可以这么写: 231 AInteger:=Aobject.Count; 232 Aobject.Count:=5; 233 实际上,编译器根据声明中的Read子句和Write子句自动把上述语句分别转换成: 234 Ainteger:=Aobject.Fcount; 235 Aobject.SetCount(5); 236 顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用With语句则可简化。 237 跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。 238 7.3.2 特性限定符 239 特性限定符可以有四类,分别是Read,Write,Stored和Default。其中Read和Write限定符用于指定访问特性的方法或字段。 240 注意:Read和Write限定符指定的方法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。 241 1.Read限定符 242 Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。 243 从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。 244 2.Write限定符 245 Write限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。 246 在Write限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。 247 从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。 248 3.Stored限定符 249 Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。 250 Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为False时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为True,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。 251 含有Stored限定符的特性声明示例如下: 252 TSampleComponent = class(TComponent) 253 protected 254 function StoreIt: Boolean; 255 public { normally not stored } 256 property Important: Integer stored True;{ always stored } 257 published { normally stored always } 258 property Unimportant: Integer stored False;{ never stored } 259 property Sometimes: Integer stored StoreIt;{ storage depends on function value } 260 end; 261 上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。 262 4.Default和NoDefult限定符 263 Default限定符用于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过Default限定符设定的,程序示例如下 : 264 TStatusBar = class(TPanel) 265 public 266 constructor Create(AOwner: TComponent); override; { override to set new default } 267 published 268 property Align default alBottom; { redeclare with new default value } 269 end; 270 ... 271 constructor TStatusBar.Create(AOwner: TComponent); 272 begin 273 inherited Create(AOwner); { perform inherited initialization } 274 Align := alBottom; { assign new default value for Align } 275 end; 276 上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。 277 注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。 278 如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。 279 7.3.3 数组特性 280 所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下: 281 type 282 TDemoComponent = class(TComponent) 283 private 284 function GetNumberName(Index: Integer): string; 285 public 286 property NumberName[Index: Integer]: string read GetNumberName; 287 end; 288 ... 289 function TDemoComponent.GetNumberName(Index: Integer): string; 290 begin 291 Result := 'Unknown'; 292 case Index of 293 -MaxInt..-1: Result := 'Negative'; 294 0 : Result := 'Zero'; 295 1..100 : Result := 'Small'; 296 101..MaxInt: Result := 'Large'; 297 end; 298 end; 299 上例中,声明了一个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。 300 对于数组特性来说,可以使用Read和Write限定符,但Read和Write限定符只能指定方法而不能是字段,并且Object Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。Write限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。 301 访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。 302 7.3.4 特性重载 303 所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。 304 最简单的重载,就是在派生类中这么写: 305 Property 特性名: 306 这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。 307 特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例: 308 type 309 TBase = class 310 ... 311 protected 312 property Size: Integer read FSize; 313 property Text: string read GetText write SetText; 314 property Color: TColor read FColor write SetColor stored False; 315 ... 316 end; 317 type TDerived = class(TBase) 318 ... 319 protected 320 property Size write SetSize; published property Text; 321 property Color stored True default clBlue; 322 ... 323 end; 324 对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,首先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了一个Default限定符。 325 7.4 类成员的可见性 326 面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么几个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。如 327 TBASE = class 328 private 329 FMinValue: Longint; 330 FMaxValue: Longint; 331 procedure SetMinValue(Value: Longint); 332 procedure SetMaxValue(Value: Longint); 333 function GetPercentDone: Longint; 334 protected 335 procedure Paint; override; 336 public 337 constructor Create(AOwner: TComponent); override; 338 procedure AddProgress(Value: Longint); 339 property PercentDone: Longint read GetPercentDone; 340 published 341 property MinValue: Longint read FMinValue write SetMinValue default 0; 342 property MaxValue: Longint read FMaxValue write SetMaxValue default 100; property Progress: Longint read FCurValue write SetProgress 343 end; 344 上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表示它们是私有的,Public部分声明的几个方法是公共的。 345 再请看下面的例子: 346 TBASE = class 347 FMinValue: Longint; 348 FMaxValue: Longint;� 349 private 350 procedure SetMinValue(Value: Longint); 351 procedure SetMaxValue(Value: Longint); 352 function GetPercentDone: Longint; 353 protected 354 procedure Paint; override; 355 public 356 constructor Create(AOwner: TComponent); override; 357 procedure AddProgress(Value: Longint); 358 property PercentDone: Longint read GetPercentDone; 359 published 360 property MinValue: Longint read FMinValue write SetMinValue default 0; 361 property MaxValue: Longint read FMaxValue write SetMaxValue default 100; 362 property Progress: Longint read FCurValue write SetProgress; 363 end; 364 上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是用{$M+}状态编译的基类,其可见性为为Published,否则就是Public。 365 7.4.1 Private 366 在Private部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,Private部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。 367 7.4.2 Public 368 在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的Public部分声明,然后把Unit1加到Init2的Interface部分就可以了。 369 注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的Public部分,以防止程序意外地不正确地修改了数据。 370 7.4.3 Published 371 在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,Published和Public的区别在于成员的运行期类型信息不同。一个Published元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有Published接口,该接口被一些Delphi工具使用,例如Object Inspector。 372 注意:只有当编译开关$N的状态为$M+时或者基类是用$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M用于控制运行期类型信息的生成。 373 7.4.4 Protected 374 Protected与Private有些类似。在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。 375 在Protected部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。 376 7.4.5 Automated 377 C++的程序员可能对这个保留字比较陌生,在Automated部分声明的成员类似于在Public部分声明的成员,它们都是公共的,唯一的区别在于在Automated部分声明的方法和特性将生成OLE自动化操作的类型信息。 378 注意:Automated只适用于基类是TAuto0bject的类声明中,在Automated部分声明的方法,其参数和返回类型(如果是函数的话)必须是可自动操作的。在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可自动操作的,否则将导致错误。可自动操作的类型包括:Byte、Currency、Double、Integer、Single 、SmallInt、String、TDateTime、Variant、WordBool等。 379 在Automated部分声明的方法只能采用Register调用约定,方法可以是虚拟的但不能是动态的。在Automated部分声明的特性只能带Read和Write限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和Write指定的只能是方法而不能是字段,方法也只能采用Register调用约定,也不允许对特性重载。 380 在Automated部分声明的方法或特性分配一个识别号(ID),如果不带DispId限定符,编译器自动给方法或特性分配一个相异的Id,如果带DispId限定符,注意Id不能重复。 381 7.5 类类型的兼容性 382 一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码: 383 type 384 TScreenThing = Class 385 X,Y:Longint; 386 procedure Draw; 387 end; 388 T3DScreenThing = Class(TScreenThing) 389 Z:Longint; 390 end; 391 procedure ResetScreenThing(T:TScreenThing); 392 begin 393 T. X:=0; 394 U. Y:=0; 395 V. Draw; 396 end; 397 procedure Reset3DScreenThing(T:T3DScreenThing); 398 begin 399 T. X:=0; 400 T.Y:=0; 401 T.Z:=0; 402 U.Draw; 403 end; 404 var 405 Q:TScreenThing; R:T3DScreenThin; 406 begin 407 {...} 408 ResetScreenThing(Q); 409 ResetScreenThing(R); {this work} 410 Reset3DScreenThing(Q); { but this does not} 411 在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。 412 7.6 VCL类结构 413 我们介绍过的Delphi的VCL构件都是使用类类型定义的对象。在Delphi中,所有的类都是从一个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了一些操纵类的最基本的方法,是Delphi所有类的缺省祖先类。使用View|Browse命令,可以打开Browse Object命令,查看Delphi各对象之间的继承关系。 414 TObject类是一切构件类和对象的基类,位于继承关系的最顶层。TPersistent类是TObject类的下一级继承者,它是一个抽象类,主要为它的继承者提供对流的读写能力。 415 TComponent类是TPersistent类的下一级继承者,它是VCL中所有构件的祖先类。TComponent类定义了构件最基本的特性、方法和事件。尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有几个非可视的构件,如TTime构件和TDataSource构件等,绝大多数构件是从TComponent类的下级TControl类继承下来的,从TControl类继承下来的都是可视化的构件,这些构件也称为控制。TControl类定义了VCL中所有可视化构件基本的特性、方法和事件等。 416 TWinControl和TGraphicControl类都是TControl类的子类。TWinControl的子类主要是用于窗口控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗口,有自己的句柄,占用Windows资源,并且可以与用户交互。而TGraphicControl的子类没有窗口句柄,也不占用Windows资源类,也能接受键盘的输入,它们的主要优点在于节约资源,如TLabel和TSpeedButton等构件。 417 实际上,整数类型可以分为基本整数类型(Fundamental type)和一般整数类型(generic type)。一般整数类型(generic type)包括Integer和Cardinal两种。在实际编程时,请尽量区分这两种,因为底层CPU和操作系统对结果进行了优化。 418 表6-2列出了Integer和Cardinal的取值范围及存储格式。 419 表6-2 一般整数类型的取值范围及存储格式 420 数据类型 取值范围 存储格式 421 Integer ?147483648..2147483647 32位有符号 422 Cardina l0..4294967295 32位无符号 423 基本整数类型包括Shortint、Smallint、Longint、Int64、Byte、Word和Longword。表6-3列出了它们的取值范围及存储格式。 424 表6-3 基本整数类型的取值范围及存储格式 425 数据类型 取值范围 存储格式 426 Shortint -128..127 signed 8-bit 427 Smallint -12768..32767 signed 16-bit 428 Longint -2147483648..2147483647 signed 32-bit 429 Int64 -2^63..2^63? signed 64-bit 430 Byte 0..255 unsigned 8-bit 431 Word 0..65535 unsigned 16-bit 432 Longword 0..4294967295 unsigned 32-bit 433 一般整数类型的实际范围和存储格式随着Object Pascal的不同实现而不同,但通常根据当前CPU和操作系统来采取最佳的操作方式。 434 一般整数类型是最常用的整数类型,可以充分利用CPU和操作系统的特性,因此程序中应当尽量使用一般整数类型。基本整数类型独立于操作系统和CPU,只有当应用程序的数据范围或存储格式特别需要时,才使用基本整数类型。 435 通常情况下,整数的算术运算结果为Integer类型,等价于32位的Longint类型。只有当操作数存在 Int64类型时,才返回Int64类型的值。因此,下面的代码将产生错误的结果: 436 var I: Integer; J: Int64; 437 ... 438 I := High(Integer); 439 J := I + 1; 440 在这种情况下,要取得一个Int64的值,必须进行类型转换: 441 J := Int64(I) + 1; 442 注意:绝大多数例程在遇到Int64时都把它转换为32位。但例程High,Low,Succ,Pred,Inc,Dec,IntToStr和IntToHex则完全支持Int64参数。Round,Trunc,StrToInt64,和StrToInt64Def函数可以返回Int64类型的结果。 443 (2)字符类型(Char) 444 字符类型中Char类型设计来只存储一个字符。一个字符占一个字节,因此Char数据类型可以存储256个不同的字符,其对应的整数为0到255。 445 除了Char数据类型外,Delphi还提供了Char类型的扩展,即AnsiChar和WideChar型。表6-4是字符数据类型的列表。 446 表6-4 字符整数类型 447 字符类型 占用字节数 存储内容 448 AnsiChar 1 存储一个Ansi字符。 449 WideChar 2 存储一个UniCode字符。 450 Char 1 目前,对应AnsiChar。但Delphi将来的版本可能对应于WideChar。 451 Ansi字符集是扩展的ASCII字符集,仍然占一个字节。目前,Char对应AnsiChar,但Borland公司在将来的Delphi版本中可能使Char对应WideChar。 452 WideChar用来支持泛字符集(Unicode)。Unicode字符占用两个字节,可以有65536种不同的取值,可以表达现代计算机中使用的世界上所有的字符,包括图形符号和用于出版业的特殊符号等。 453 UniCode字符集的前256个字符对应着ANSI字符。如果你把一个AnsiChar字符放到WideChar字符类型的变量中,WideChar字符类型变量的高字节将全部置为0,AnsiChar字符存放到WideChar字符类型的变量的低字节中。 454 注意:Windows NT全面支持Unicode字符号集,但Windows 95却不同。如果你希望书写的程序同时能在两种系统上运行,必须使用SizeOf()函数,以确定字符占多少字节。 455 (3)布尔类型(Boolean) 456 Boolean数据类型的变量只存储一个逻辑值,例如True或False。共有4种Boolean数据类型,见表6-5。 457 表6-5 布尔类型 458 类型 说明 459 Boolean 占1个字节 460 ByteBool 占1个字节 461 WordBool 占2个字节 462 LongBool 占4个字节 463 Delphi提供多种Boolean数据类型的目的是为了兼容,因为在某些情况下,Windows需要用一个字(2个字节)或双字(4个字节)来表示一个布尔值。 464 (4)枚举型 465 所谓枚举类型,就是用一组数量有限的标识符来表示一组连续的整数常数,在类型定义时就列出该类型可能具有的值。枚举类型是一种用户自定义的简单数据类型。在类型定义时就列出该类型可能具有的值。下面是枚举类型定义的一些例子:type 466 TDays=(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday); 467 TPrimaryColor=(Red,Yelloow,Blue); TDepartment=(Finance,Personnel,Engineering,Marketing,MIS); TDog=(Poodle,GoldenRetriever,Dachshund,NorwegianElkhound,Beagle); 468 枚举类型定义中的每个值都对应一个整数,整数值由该值在类型定义表中的位置决定,通常类型定义的第一个数对应的整数值为0。例如,在TDay类型定义中Monday对应值为0、Tuesday值为1,等等。如果你把DayOfWeek定义为Integer,通过赋整数值来代表星期几,也可以得到同样的结果。但是,由于枚举类型表达的意思明确、直观、便于记忆,因此使用枚举类型仍有必要。 469 下面是声明一个枚举类型的语法(图6.2)。 470 其中标识符列表中的标识符之间用逗号隔开,它列出该类型可能具有的值。 471 下面是声明一个枚举类型变量的举例: 472 var DayOfWeek:TDays; 473 Hue:TPrimaryColor; 474 Department:TDepartment; 475 Dogbreed:TDog; 476 也可以把类型声明和变量声明合二为一,例如: 477 var DayOfWeek:(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday); 478 在声明枚举类型和枚举变量时,请注意以下几点: 479 1)枚举的元素只能是标识符,标识符的命名必须符合 Pascal关于标识符的规定,例如下面的声明就是错误的: 480 type TDays=(0,1,2,3,4,5,6,7); 481 2)同一个枚举元素不能出现在多个枚举类型中。例如下面的声明就是错误的: 482 type TColors1=(Red,Blue,Green,White,Black); 483 TColors2=(Yellow,Lime,Silver,Green); 484 两个类型变量中都有Green元素,是不允许的。 485 3)不能直接用枚举类型中的元素参加运算,例如,下面的语句就是错误的: 486 X:=Red+Blue; 487 但是,可以用某个枚举类型中的元素对枚举变量赋值,例如,下面的语句: 488 DayOfWeek:=Monday; 489 (5)子界型 490 子界类型是Integer,Boolean,Char及枚举型等称为宿主类型数据的一个子集。当你要限制一个变量的数据范围时,使用子界类型就特别有用。子界类型也是一种用户自定义的简单数据类型。要定义子界类型,必须说明区间的最大值和最小值,下面是子界类型定义的一些例子: 491 type TCompassRange = 0..360; 492 TValidLetter ='A'..'F' 493 TMonthlyIncome=10000..30000; 494 THours =0..23; TPrecipitation=(Drizzle,Showers,Rain,Downpour,Thunderstorm); {枚举型} 495 TRain =Drizzle..Downpour; {上面枚举型的子界型} 496 下面是声明一个子界类型的语法规则(图6.3)。 497 其中两个常数(称为上界和下界)必须是同一种有序类型,如Integer,Boolean,Char及枚举型等,但不能是Real数据类型。第一个常数必须小于或等于第二个常数。 498 下面是声明子界类型变量的举例: 499 var Compass:TCompassRange; 500 ValidChar:TValidLetter; 501 在声明子界类型和子界类型变量时,请注意以下几点: 502 1)上界常数和下界常数必须是同一类型,且都是有序类型。 503 2)子界类型变量具有宿主类型数据的所有运算特性,但运算的结果必须在范围内。 504 3)上界常数和下界常数可以是表达式。例如: 505 const X = 10; Y = 50; 506 type Color = (Red, Green, Blue); 507 Scale = X * 2..X * 20; 508 2.实数类型(Real) 509 实数类型是带有小数部分的数值,存储实数。有6种不同的Real数据类型,它们在范围、精确度、大小等方面都不相同。见表6-6。 510 实数类型 511 数据类型 取值范围 有效位 存储字节 512 Real48 2.9 x 10^-39 .. 1.7 x 10^38 11..12 6 513 Single 1.5 x 10^-35 .. 3.4 x 10^38 7..8 4 514 Double 5.0 x 10^-324 .. 1.7 x 10^308 15..16 8 515 Extended 3.6 x 10^-4951 .. 1.1 x 10^4932 19..20 10 516 Comp -2^63+1 .. 2^63 ? 19..20 8 517 Currency 22337203685477.5808..922337203685477.5807 19..20 8 518 当前通常使用的Real等价与Double。 519 6.3.2 字符串类型 520 Delphi在处理字符串时,提供了多种方式,表6-7是Delphi使用的字符串类型。 521 表6-7 字符串类型 522 类型 最大长度 存储的字符 是否以Null结尾 523 ShortString 255个字符 AnsiChar 否 524 AnsiString~ 2^31个字符 AnsiChar 是 525 String 或者255或者~2^31个字符 ANSIChar都可能 526 WideString ~2^30个字符 WideChar 是 527 从上表可知,Delphi主要支持两种类型的字符串: ShortString和AnsiString。WideString类似于AnsiString,只是其存储的字符为WideChar。 528 ShortString数据类型定义的是长度在1到255之间动态长度字符串。像数组一样,单个字符可以通过引用它们的索引进行存取。位于0的字节存储了代表字符串当前所赋值长度的数值(只能通过关闭范围检查才能访问)。ShortString数据类型主要是为了能和Delphi 1.0和Borland Pascal的早期版本兼容。 529 AnsiString(又称为long String或huge String)数据类型的定义是动态分配的,长度几乎可以无限的(仅受可用内存限制)以NULL结尾的字符串。AnsiString中的字符由AnsiChar数据类型的字符组成。 530 建议最好使用AnsiString数据类型。这是因为AnsiString数据类型的变量是动态分配的,当把一个更长的字符串放入AnsiString数据类型的变量时,Delphi会从新为该变量申请空间。如果要显式地改变字符串的长度,可以使用SetLength() 函数来分配一块恰当的内存;使用AnsiString数据类型的另外一个优点是,AnsiString字符串是以NULL结尾,即在最后一个字符之后自动加上一个NULL字符表示字符串结束,与操作系统的大多数函数例程兼容,例如Win32 API,从而在调用操作系统函数例程时更加方便,不需要使用StrPCopy()来将以Pascal风格的字符串转换为以NULL结尾的字符串。Delphi VCL构件的所有特性、事件使用AnsiString来传递参数,以简化、统一VCL和API之间的接口。 531 String既可以是SHortString类型也可以是AnsiString类型,缺省是AnsiString类型。例如,如果你像下面那样定义字符串: 532 var S: String;// S is an AnsiString 533 则编译器假定你要创建一个AnsiString数据类型变量。 534 使用$H编译命令可以改变缺省定义。当在程序中把编译开关$H的状态改为{H-}时,String缺省是ShortString类型;当在程序中把编译开关$H的状态改为{H+}时,String缺省是AnsiString类型。例如: 535 var {$H-} S1: String; // S1 is a ShortString 536 {$H+} 537 S2: String; // S2 is an AnsiString 538 如果定义中指明了长度(最大为25 5),则String为ShortString。例如: 539 var S: String[63]; // S是一个 长度为63的ShortString变量。 540 6.3.3 结构数据类型 541 结构类型在内存中存储一组相关的数据项,而不是像简单数据类型那样单一的数值。Object Pascal结构类型包括集合类型、数组类型、记录类型、文件类型、类类型、类引用类型、接口类型等。这里,我们只介绍集合类型、数组类型、记录类型和文件类型。类类型、类引用类型和接口类型放在下一章介绍。 542 1.数组(Array) 543 数组是一种数据类型数据的有序集合,是代表一定数量具有相同类型变量的一种数据类型。Object Pascal数组可与任何简单数据类型或字符串类型等一起使用。数组可用于声明一个简单变量或作为一个记录类型定义的组成部分。 544 (1)数组的定义 545 下面是声明一个数组类型的语法规则(图6.4)。 546 要声明一个数组变量,要求你提供一个标识符,使用array保留词,在方括号中指定数组的界限,并指定编译器数组将用于存储什么类型,例如: 547 Var Check:array[1..100] of Integer; 548 范围标点‘..’用于表示Check是一个有100个整数的数组,这些整数从1到100编号。范围编号是一个子界类型,可以是0,也可以是正数或负数,或者字符,或其它有序类型。 549 下面是声明数组类型及数组类型变量的举例: 550 Type TCheck = array [1..100] of Integer; 551 Var CheckingAccount:TCheck; 552 上面是先定义数组类型,然后定义数组变量。其实上,也可以同时定义类型、变量,例如: 553 var Kelvin:array[0..1000] of Temperatures; 554 TwentiethCentury: array[1901..2000] of Events; 555 LessThanzeroo: array[-999..-400] of Shortint; 556 DigiTValues:array ['0'..'9' of Byte; 557 SecretCode:array[''A'..'Z' of char; 558 访问数组中的元素很简单,只要在标识符后面的方括号中给出指定的元素的索引号即可。例如: 559 Check[5]:=12; 560 J:=9; 561 Check[J]:=24; 562 要访问数组中的所有元素,可以使用循环语句。例如 : 563 For J:=1 to 10 do Check[J]:=0; 564 (2)多维数组 565 上面介绍的是一维数组。实际上,数组可以是多维的。例如,如果你想编写一个数组来容纳一张电子表格中的值,那么就可以使用2维数组。下面的例子说明如何使用2维数组定义一个有20行和20列的表格: 566 Type Ttable = array[1..20,1..20] of Double; 567 Var BigTable:Ttable; 568 要将2维数组中的所有数据初始化,可以使用如下语句: 569 Var Col,Row:Intger; 570 . 571 . 572 . 573 for Col:=1 to 20 do 574 for Row:=1 to 20 do 575 BigTable[Col,Row]:=0.0; 576 使用多维数组时,要记住的一件事是数组为每维所占据的RAM数都呈幂级数增加。例如: 577 Aline:Array[1..10] of byte;占用10个字节 578 AnArea:Array[1..10,1..10] of byte;占用10*10=100个字节 579 Avloume:Array[1..10,1..10,1..10] of byte;占用10*10*10=1000个字节 580 (3)字符数组 581 前面介绍的字符串,实际上就是一个1维字符数组,只不过Pascal对字符串类型作了特殊的准许,你可以把它看作一个整体。字符串类型本质上等同于下列类型:type StringType:array[0..255] of char;但是,虽然你可以把一个字符串看待,但它仍然保持其数组的特性。例如在定义一个字符串类型变量时,你可以说明字符串的大小,就像你定义字符数组的大小一样。下面是几个字符串类型定义: 582 type MyString:string[15]; 583 BigString:string; 584 LittleString:string[1]; 585 上面语句定义MyString类型包含15个字符,LittleString包含1个字符,BigString没有说明大小,就取字符串包含字符的最大个数255。然后你可以定义这些类型的变量,就像使用其它类型一样: 586 Var MyName:MyString; 587 Letter,Digit:LittleString; 588 你可以对字符串变量进行赋值: MyName:='Frank P.BorLand'; 589 ?因为MyName长度为15,因此只能容纳15个字符。如果执行下面语句: MyName:=Frank P.Borland?则MyName变量中只存有FranK.P.Borlan其余部分被舍弃。 590 为了取得字符串中的一个字符,可以按如下方法进行:AChar:=MyName[2]; 591 但是,如果索引大于字符串变量的长度,则结果不可知。例如: AChar:=MyName[16];则AChar将被设置为某个不确定的字符,换句话说,就是废字符。 592 在字符串类型的数组中,字符串的第一个位置[0]包含有字符串的长度,因此数组的实际长度比该字符串长度大1个字节。你可以使用Length函数或下面的代码来得到一个字符串的长度:L:=Ord(String[0]); 593 (4)数组类型常量 594 数组类型常量的每个字段都是类型常量,下面是声明数组类型常量的语法规则(图6.5)。 595 从图中可以知道,一个数组类型常量由括号括起来的类型常量组成,不同类型常量用逗号隔开。 596 像简单类型常量一样,数组类型常量用来定义一个数组常量,下面是一个例子。 597 type TStatus = (Active, Passive, Waiting); 598 TStatusMap = array[TStatus] of string; 599 const StatStr: TStatusMap = ('Active', 'Passive', 'Waiting'); 600 上面的例子首先定义一个数组TStatusMAp,然后定义一个数组常量StatStr。该数组常量的目的是把TStatus类型的值转化为对应的字符串。下面是数组常量StatStr元素的值: 601 StatStr[Active] = 'Active'StatStr[Passive] = 'Passive' 602 StatStr[Waiting] = 'Waiting' 603 数组常量的元素类型可以是除文件类型以外的任何类型。字符数组类型常量既可以是字符也可以是字符串,例如: 604 const Digits: array[0..9] of Char = ('0', '1', '2', '3', '4', '5','6', '7', '8', '9'); 605 该数组常量也可以表示为:const Digits: array[0..9] of Char = '0123456789'; 606 初始化字符数组类型常量的字符串长度可以小于数组类型的定义长度,例如:var FileName: array[0..79] of Char = 'TEST.PAS';这时数组余下的字符空间自定置NULL(#0),因此数组也变成了一个以NULL结尾的字符串。 607 多维数组类型常量的定义采用括号的形式,每一维用括号括起,不同维及不同元素常量之间用逗号隔开。最里面的常量对应最右面的维数。 608 例如:type TCube = array[0..1, 0..1, 0..1] of Integer;const Maze: TCube = (((0, 1), (2, 3)), ((4, 5), (6, 7))); 609 Maze常量数组各元素的值为: 610 Maze[0, 0, 0] = 0 611 Maze[0, 0, 1] = 1 612 Maze[0, 1, 0] = 2 613 Maze[0, 1, 1] = 3 614 Maze[1, 0, 0] = 4 615 Maze[1, 0, 1] = 5 616 Maze[1, 1, 0] = 6 617 Maze[1, 1, 1] = 7 618 (5)开放式数组 619 所谓开放式数组,是指数组作为形参传递给过程或函数时其长度是可变的,这样在调用过程或函数时,可以传递不同长度的数组作为实际参数。 620 开放式数组在过程或函数中作为形参可以定义为: array of T这里T是数组的元素类型标识符,实际参数必须是T类型的变量或元素类型为T的数组变量。在过程或函数内形参的作用可看作为下面的数组: array[0..N - 1] of T 621 这里N是实参中元素的个数。实际上实参的上下界被映射到0到 N-1。如果实参是类型T的简单变量,则它被看成为只有类型T元素的数组。 622 开放数组只能以开放数组参数或一个未定义变量参数的的形式传递到过程或函数。开放数组可以作为数值参数、常数参数或变量参数,并与这些参数具有同样的语法规则。作为形式参数的开放数组不允许整体赋值,只能访问它的元素。并且对元素的赋值不影响实参。 623 当开放式数组作为数值参数时,编译器将在内存中开辟一块区域存放实参的拷贝,等过程或函数退出后再释放这块区域,这样当实参是个很大的数组时,可能会发生栈溢出的问题。在使用开放数组参数时,可以使用Low函数获得当前最小下标(不过总是为0),使用High函数获得当前最大下标,使用SizeOF函数获得当前数组大小。下面是一个例子,演示了开放式数组的使用。 624 {定义两个长度不同的数组变量} 625 Var X1:array[1..10] of Double; 626 X2:array[1..30] of Double; 627 {Clear过程对一个Double数组的各元素清0,SUM过程计算一个Double数组的各元素之和。两个过程的参数都是开放式数组。} 628 procedure Clear(var A: array of Double); 629 var I: Integer; 630 begin 631 for I := 0 to High(A) do 632 A[I] := 0; 633 end; 634 function Sum(const A: array of Double): Double; 635 var I: Integer; S: Double; 636 begin 637 S := 0; 638 for I := 0 to High(A) do S := S + A[I]; 639 Sum := S; 640 end; 641 begin 642 Clear(X1); 643 Clear(X2); 644 Sum(X1); 645 Sum(X2); 646 end; 647 当开放式数组的元素类型为Char时,实参可以是一个字符串常数。例如: 648 procedure PrintStr(const S: array of Char); 649 var I: Integer; 650 begin 651 for I := 0 to High(S) do 652 if S[I] <> #0 then Write(S[I]) 653 else Break; 654 end; 655 下面是合法的过程调用语句: 656 PrintStr('Hello world'); 657 PrintStr('A'); 658 (6)动态数组 659 在Delphi中,除了定义静态数组外,还可以定义动态数组。动态数组只需说明数组的类型信息(包括数组的维数和数组元数的类型),但不需要定义元素的个数。例如: 660 A: array[1..100] of string;//静态数组 661 B: array of integer//动态数组 662 C: array of array of string;//动态数组 663 这里A是静态数组,B是一维的整数动态数组,C是二维的字符串动态数组。 664 动态数组没有固定的长度。相反,当为动态数组赋值或使用SetLength过程时,动态数组的内存空间将重新分配。动态数组的定义形式是: 665 array of baseType 666 例如: var MyFlexibleArray: array of Real; 667 定义了一个类型为实数型的一维动态数组。注意,声明语句并没有为MyFlexibleArray分配内存。要为动态数组分配内存,需要调用SetLength过程。例如: 668 SetLength(MyFlexibleArray, 20);上面语句分配20个实数,标号从0到19。 669 动态数组的标号是整数类型,标号总是从0开始。使用Length,High和Low函数可以取得有关动态数组的特性。Length函数返回数组中元素的个数。High函数返回数组的最大标号,Low返回0。 670 2.集合类型 671 集合类型是Integer,Boolean,Char,枚举型,子界型等类型数据的一个子集。在应用程序中,当要检测一个数是否属于一个特定的集合时,就可以使用集合类型。(1)集合类型的定义下面是声明一个集合类型的语法规则(图6.6)。 672 其中Set of是保留字,ordinal Type是集合的基类型,可以是任何有序类型如整数型,布尔型,字符型,枚举型和子界型,但不能是实型或其它自定义类型。下面是一些集合类型的例子: 673 type VoterDataSet= Set Of (Democrat,Republican,Male,Female, LowOpinion,HighOption,Confused); 674 Chars = Set of Char; 675 Letters = Set of 'a'..'z'; 676 VIBGYOR= (Violet,Indigo,Blue,Green,Yellow,Orange,Red); {这是枚举型} 677 ColorSet = set of VOBGYOR;{上面枚举型的集合类型} 678 一个集合类型的变量的值实际上是它的基类型的一个子集,可以为空集。一个集合最多可有256个元素。因此下面的集合定义是错误的: 679 type SET1= Set Of Integer; 680 这是因为Integer集合的元素个数远远大于256。 681 下面是集合类型变量的一些例子: 682 var Voter: VoterDataSet; 683 Color: ColorSet; 684 Lets:Letters; 685 Pascal使用方括号来表示集合,例如: 686 [Democrat];表示只含Democrat的集合。 687 一个集合可以拥有0个元素,这时称之为空集,用两个方括号表示,其中什么也没有。对于集合类型变量,你可以进行+,-,=,*(并),IN等运算。见下表6-8。 688 表6-8 集合类型运算 689 操作符 描述 举例 690 + 往一个集合中添加元素 Aset:=Aset+AnotherSet; 691 - 从一个集合中去除元素 Aset:=Aset-AnotherSet; 692 * 去除两个集合中都没有的元素 Aset:=Aset*AnotherSet; 693 In 测试元素 Bool:=AnElement in Aset 694 下面是集合运算的例子: 695 Voter:=[HighOption]; 696 Voter:=Voter+[Democrat]; 697 Voter:=Voter+{male}; 698 Voter:=Voter-[HighOption]; 699 If HighOption in Voter then SendtheVoterFlowers; 700 (2)集合类型 701 常量像简单类型常量一样,集合类型常量用来定义一组常量的集合。例如: 702 type TDigits = set of 0..9; 703 TLetters = set of 'A'..'Z'; 704 const EvenDigits: TDigits = [0, 2, 4, 6, 8]; 705 Vowels: TLetters = ['A', 'E', 'I', 'O', 'U', 'Y']; 706 上面的例子首先定义两个集合类型TDigits和Tletters,然后定义了两个集合常量,其中EvenDigits的值域是[0, 2, 4, 6, 8],它为TDigits的一个子集;Vowels的值域是 ['A', 'E', 'I', 'O', 'U', 'Y'],它为TLetters的一个子集。 707 3.记录类型 708 记录是一系列相关的变量,这些变量被称为域,它们放在一起,作为一个整体使用。例如,一个雇员可能包含姓名、雇用时间、薪金等数据,这时你可以像下面那样定义一个雇员记录类型: 709 type TEmployee = record LastName: String[20]; 710 FirstName:String[15]; 711 YearHired:1990..2000; 712 Salary:Double; 713 Position:string[20]; 714 end; 715 Pascal的记录类型跟数据库中的记录很相似,记录类型中的元素可以理解为数据库中的字段,事实上Pascal正是借用了数据库中的记录和字段的概念。 716 (1)记录类型的定义 717 下面是声明记录类型的语法规则 718 记录可以一个字段也没有,即为空记录;一个记录可以有一个固定部分(fixed part),在固定部分,每个字段都有其确定的标识符和数据类型,它们在内存中分别占用不同的区域;一个记录也可以加入一个可变部分(variantpart)。声明记录变量与声明其它类型变量一样,下面是两个记录变量的说明: 719 var NewEmployee,PromotedEmployee:TEmployee; 720 记录类型中的每个域都有一种数据类型,你既可以单独访问这些域,也可以把记录作为一个整体来使用。例如,你可以像下面那样访问NewEmployee记录中的Salary域或整个记录: 721 NewEmployee.Salary:=43211.00; 722 PromotedEmployee:=NewEmployee; 723 当你要访问记录内的域时,需要指定记录名,并在记录名后加(.),然后跟上域名。例如: 724 PromotedEmployee.Position 725 如果要对多个域赋值,则每个域前都必须加记录名。例如: 726 PromotedEmployee.LastName :='Gates' 727 PromotedEmployee.FirstName:='Bill' 728 PromotedEmployee.YearHired:=1990; 729 PromotedEmployee.Salary:=92339.00; 730 PromotedEmployee.Position:='Manager' 731 ?Pascal提供了With语句,使你可以减少重复书写记录名的烦恼。With语句的语法是: With记录变量名Do ... 732 每个可变部分由至少由一个常量(Constant)标识,所有常量必须是唯一的,并且类型为与tag field type指定类型相容的类型。Identifier用于记录可变部分的可选部分,称为识别字段标识符。如果定义了识别字段标识符,程序可以使用该标识符决定在给定的时间内哪个可变部分是活动的,如果没有定义识别字段标识符,程序必须根据其它规则选定记录可变部分。 733 下面是带有可变部分的记录类型的例子: 734 type TPolygon = record X, Y: Real; 735 case Kind: Figure of 736 TRectangle: (Height, Width: Real); 737 TTriangle: (Side1, Side2, Angle: Real); 738 TCircle: (Radius: Real); 739 End; 740 注意:记录可变部分的字段不能是长字符串类型和变体类型,也不能含有长字符串类型和变体类型分量的构造类型。 741 (2)记录类型常量 742 记录常量的每个字段都是类型常量, 743 一个记录类型常量每个字段由一个标识符和类型常量组成,不同字段用分号隔开,字段部分用括号括起。像简单类型常量一样,记录类型常量用来定义一个记录常量,下面是一些例子。 744 type TMonth = (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); 745 TDate = record D: 1..31; 746 M: Month; 747 Y: 1900..1999; 748 end; 749 const 750 SomeDay: TDate = (D: 2; M: Dec; Y: 1960); 751 上面的例子首先定义一个记录类型TDate,然后定义了一个记录常量SomeDay。注意:记录类型常量中个字段的出现顺序必须与记录类型定义中的顺序一致。如果记录类型包括文件类型字段,则不能定义该记录的记录常量。如果记录类型包括可变部分,则只有被选择的可变部分可以定义常量。 752 4.文件类型 753 文件是指相同类型元素的有序集合。Delphi处理文件有三种方式,一种是使用Object Pascal标准的文件处理技术;一种是使用Windows的文件处理函数;还有一种是使用文件流对象。 754 Object Pascal标准的文件处理技术,有专门的数据类型和文件处理例程,并且与Windows的标准API不兼容,但对于熟悉Pascal的用户来说,仍然是操作文件的好选择。下面我们就对此进行介绍。 755 声明一个文件类型的语法如下: type fileTypeName = file of type 756 这里,fileTypeName是任何有效的标识符, type是一种大小固定的数据类型,称之为基类型。 基类型不能使用指针类型,不能包括动态数组、长字符串、类、对象、指针、可变类型以及其它文件类型。但可以是其它结构类型,例如: 757 type 758 PhoneEntry = record FirstName, LastName: string[20]; 759 PhoneNumber: string[15]; 760 Listed: Boolean; 761 end; 762 PhoneList = file of PhoneEntry; 763 这里,PhoneEntry是自定义的结构类型,PhoneList是以PhoneEntry为基类型的文件类型。在定义了文件类型后,就可以直接定义文件类型的变量了。例如: 764 var List1: PhoneList; 765 有时侯,我们也可以使用file of基类型的方式直接定义文件类型变量。例如下面的定义与上面的形式有同样的效果: 766 var List1: file of PhoneEntry; 767 如果我们在声明文件类型时不指明其基类型。则这样的文件我们称之为无类型文件,如: 768 var DataFile: file; 769 无类型文件主要用于直接访问磁盘文件的多层I/O操作。6.3.4 指针类型指针类型对程序员来说可能是最复杂和最灵活的数据类型。当你在Delphi中创建一个数据结构时,首先要分配内存空间。分配的内存空间用于存储数据结构中的数据。而指针就是指向分配空间的内存地址。使用指针,可以使程序不必每次需要时都去分配,只要申请一次即可,其它过程或函数使用同一块内存空间时,只要使用该内存空间的地址。例如,假设你的一个邻居问你怎样去百货店,你并不需要把整个百货店搬到邻居家里,只需要告诉它去百货店的路径即可,这个路径类似于一个指针。 770 1.指针类型的声明 771 声明指针类型的语法规则见图6.10。 772 其中基类型可以是简单类型,也可以是前面介绍的结构,或只是一个标识符。如果基类型是一个未定义的类型标识符的话,则该类型标识符必须在同一块内声明。 773 下面是带有指针类型声明的例子: 774 type WordPtr =^Word; 775 RealPtr =^Real; 776 PersonType=Record LAstNAme:String; 777 FirstNAme:String; Age:Integer; 778 end; 779 PersonPointer = ^PersonType; 780 上例中,声明了三个指针类型,一个是WordPtr,指向^Word,一个是RealPtr,指向^Real,还有一个是PersonPointer,指向一个标识符,而该标识符标识一个记录类型。 781 声明了指针类型之后,就可以声明指针类型变量, 782 例如:Var WP:WordPtr; 783 RP:RealPtr; 784 Person:PersonPointer; 785 2.指针的使用 786 Delphi提供专门的过程和函数操作指针,这些过程和函数是:New过程,@操作符,PTR函数,GetMem过程。下面分别介绍。 787 (1)New过程 788 New过程是 Pascal中的标准例程(在System单元声明),用于在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量。New过程的语法为: 789 procedure New(var P: Pointer); 790 其中P是一个指针变量。所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。 791 新分配的内存空间由P指向,P^即为类型的动态变量。应用程序不再需要该动态变量时,可以调用Dispose标准例程释放为该变量分配的内存空间。 792 下面是使用New过程的举例: 793 type PListEntry = ^TListEntry; 794 TListEntry = record 795 Next: PListEntry; 796 Text: string; 797 Count: Integer; 798 end; 799 var List, P: PListEntry; 800 begin 801 ... 802 New(P); 803 P^.Next := List; 804 P^.Text := 'Hello world'; 805 P^.Count := 1; 806 List := P; 807 Dispose(P); 808 ... 809 end; 810 上例中,声明了一个指针类型 PListEntry,指向标识符 TListEntry,而该标识符标识一个记录类型 TlistEntry,然后定义了两个指针变量List和P。程序首先用New过程在应用程序堆栈中为动态变量申请一块区域,并把该区域的地址赋予指针变量P。P^即为记录类型TListEntry的动态变量。不再需要该动态变量后时,调用Dispose释放为该变量分配的内存空间。 811 (2)@操作符 812 @操作符是个一元操作符,用于获得操作数的地址,其使用语法见图6.11。 813 从图中可以知道,@后面的操作数可以是变量、过程、函数或类类型中的方法。 814 程序示例如下: 815 procedure ChangeValue(X:Integer) 816 Var IntPtr:^Integer; 817 begin 818 IntPtr:=@X; 819 Writeln(IntPtr^); 820 IntPtr^:=20; 821 end; 822 假设主程序如下: 823 begin 824 Param:=10; 825 ChangeValue(param); 826 Writeln(Param); {10} 827 end; 828 上例中,ChangeVAlue过程首先声明了一个指向整型数的指针Ptr,然后用@操作符取出X的地址赋予IntPtr指针,并显示Ptr指针指向的数,最后改变这个数。 829 (3)PTR函数 830 Ptr函数是 Pascal中的标准例程(在System单元声明),用于把一个指定的地址转换为指针。Ptr函数的语法为: 831 function Ptr(Address: Integer): Pointer; 832 其中Address是一个整数,用于表示一个32位地址,函数执行的结果是把32位地址转化为指针。 833 (4)GetMem过程 834 GetMem过程也是Pascal中的标准例程(在System单元声明),类似于New,用于在应用程序堆栈中为动态变量申请一块指定大小的区域,并把该区域的地址赋予指针变量。GetMem函数的语法为: 835 procedure GetMem(var P: Pointer; Size: Integer); 836 其中P是一个指针变量,Size指定区域的字节数。 837 所分配区域的大小由指针变量P的基类型决定。如果在应用程序堆栈中没有足够的内存空间供分配,将触发EOutOfMemory异常。如果程序不再需要该动态变量时,可以调用FreeMem标准例程释放为该变量分配的内存空间。 838 程序示例如下: 839 Var F: file; 840 Size: Integer; 841 Buffer: PChar; 842 begin 843 AssignFile(F, 'test.txt'); 844 Reset(F, 1); 845 try 846 Size := FileSize(F); 847 GetMem(Buffer, Size); 848 try 849 BlockRead(F, Buffer^, Size); 850 ProcessFile(Buffer, Size); 851 finally 852 FreeMem(Buffer); 853 end; 854 finally 855 CloseFile(F); 856 end; 857 end; 858 上例打开一个名字为test.txt的文件,并把文件读入动态分配的缓冲区,缓冲区大小为文件的大小,然后对文件进行处理,最后释放动态分配的缓冲区,并关闭文件。 859 Pascal中有一个特殊的保留字nil,这是一个空指针常量,当指针的值为nil时,表示指针当前没有指向任何动态变量。值为nil的指针变量不能访问动态变量。 860 3.无类型指针 861 无类型的指针是指指针变量在声明时没有指明基类型。无类型指针在声明中只使用Pointer。例如: 862 var pAnyPoint:Pointer; 863 指针pAnyPoint可以指向任何变量类型。无类型的指针的作用是它可以指向任何类型,但是,不能用指针变量符后加^的形式来引用它的动态变量。 864 4.字符指针类型 865 字符指针类型即PChar数据类型,是一个指向以NULL(不是零)字符结尾的字符(Char)串的指针。这种类型主要用于与外部函数如在Windows API中所用的函数兼容。与Pascal字符串不同,Windows和C字符串没有一个长度字节。取而代之的是它们从0字节索引开始,以一个NULL(#0)结束。Pascal RTL字符函数根据长度决定存储在字符串变量中的字符数目。C函数实际上一次搜索字符数组的一个字符,直到碰到NULL,表示字符串结尾。在Windows API中所用的许多函数以指向NULL结束字符串或用NULL结束填入缓冲区的字符。在Pascal中使用这些函数就需要PChar类型变量。内存将分配给变量并被所需函数使用。 866 除了PChar外,Delphi还包含PAnsiChar和PWideChar数据类型。 867 PAnsiChar数据类型是一个指向以NULL(不是零)字符结尾的AnsiChar字符串的指针,在Delphi中,PCHAR等同于PAnsiChar。 868 ?PWideChar数据类型是一个指向以NULL(不是零)字符结尾的WideChar字符串的指针,用于UniCode字符集。实际上,PAnsiChar和PWideChar数据类型的定义为: 869 type 870 PAnsiChar = ^AnsiChar; 871 PWideChar = ^WideChar; 872 PChar = PAnsiChar; 873 字符串类型与PCHAR类型赋值兼容,即一个字符串可以直接赋给一个PCHAR类型的变量,例如: 874 var P: PChar; 875 ... 876 begin 877 P := 'Hello world...'; 878 end; 879 上面赋值语句首先申请一块区域,该区域包含字符串 'Hello world...',并在最后加上NULL,然后P指向这块内存区。上述例子等价于下列形式: 880 const TempString: array[0..14] of Char = 'Hello world...'#0; 881 var P: PChar; 882 ... 883 begin 884 P := @TempString; 885 end; 886 6.3.4 过程类型 887 Object Pascal允许把过程和函数作为一个整体赋给变量和作为参数传递。实现这一功能的途径是使用Object Pascal的过程类型。 888 声明一个过程类型的语法与声明过程或函数的首部的语法相似,不同的是声明一个过程类型时不需要过程或函数保留字后面的标识符。声明过程类型时可以指定一种调用约定方式,缺省的调用方式是Register。下面是声明过程类型的举例: 889 type 890 TProcedure = procedure; 891 TStrProc = procedure(const S: string); 892 TMathFunc = function(X: Double): Double; 893 上例声明的三个过程类型中,第一个是不带任何参数的过程,第二个是带一个参数S的过程,第三个是带一个参数X的函数,函数返回值为Double。 894 过程类型根据其是否运用于对象分为两类:全局过程指针和方法指针。 895 声明过程类型时不带of Object的是全局过程指针。全局过程指针指向的是全局的过程或函数。例如上面的过程类型Tprocedure,TstrProc,TMathFunc都是全局过程指针。 896 声明过程类型时带有of Object的是方法指针。方法指针指向的是一个对象的过程或函数方法。例如下面的过程类型是方法指针。 897 type TMethod = procedure of object; 898 TNotifyEvent = procedure(Sender: TObject) of object; 899 声明过程类型变量的方法与声明其它类型变量的方法相同,下面例子声明两个过程类型变量: 900 var Proc:TProcedure; 901 StrProc:TStrProc; 902 过程类型变量的值可以取下列四种之一: 903 nil一个过程类型变量 904 一个全局过程或函数标识符一个方法指示符下面举例说明过程类型的用法。 905 type TMainForm = class(TForm) 906 procedure ButtonClick(Sender: TObject); 907 ... 908 end; 909 var MainForm: TMainForm; 910 MathFunc: TMathFunc; 911 OnClick: TNotifyEvent; 912 function Tan(Angle: Double): Double; 913 begin 914 Result := Sin(Angle) / Cos(Angle); 915 end; 916 上例的TMainForm是一个类类型,TMathFunc是前面定义的全局过程指针,TnotifyEvent是前面定义的方法指针。其中MathFunc和OnClick是两个过程类型变量。变量MathFunc和OnClick的赋值方式为: 917 MathFunc := Tan;OnClick := MainForm.ButtonClick; 918 调用结果为: 919 X := MathFunc(X);{等价于 X := Tan(X) } 920 OnClick(Self);{等价于 MainForm.ButtonClick(Self) } 921 过程类型变量值等于NIL表示该过程类型变量没有赋值,因此在过程语句或函数调用中使用值等于NIL的过程类型变量将发生错误。防止的办法是使用Assigned()函数。例如: 922 if Assigned(OnClick) then OnClick(Self); 923 如果给定的过程类型变量已经赋值,Assigned函数返回TRUE,如果给定的过程类型变量值为NIL,Assigned函数返回FALSE。在把一个过程或函数赋给一个过程类型变量时要注意赋值兼容,必须满足下列条件:调用约定方式必须相同。?参数个数必须相同,相应的数据类型必须相同。?函数返回的值类型必须相同。 924 6.3.5 Variant数据类型 925 Variant主要用于表达需要动态改变类型的数据。例如,当一个数据的实际类型在编译时不知道或运行时需要改变类型时,就可以使用Variant类型。 926 Variant类型变量可以包含integer, real, string, boolean, 日期和时间等类型值或以及 OLE自动化对象等,还可以表示长度和维数可变的数组。 927 Variant变量在首次创建时,总是被初始化为Unassigned。Unassigned是Variant变量的一个特殊值,表明Variant变量还未赋值,Variant变量的另一个特殊值是NULL,指示Variant变量未知或丢失数据。 928 6.4 数据类型的转换 929 Object Pascal是一种类型严谨的程序设计语言,不是所有类型的数据都可以互相赋值的。只有赋值两边的数据类型一致或兼容才可以进行赋值操作。下面就有关数据类型兼容和强制数据类型转换等概念进行介绍。 930 6.4.1 类型兼容 931 所闻类型兼容,是指一种类型的数据可以与另一种类型的数据进行关系运算。类型兼容是赋值兼容的前提条件,也是Object Pascal数据运算的基本前提。 932 ?Object Pascal规定,只有满足下列条件才是类型兼容: 933 ?两种类型都一致。 934 ?两种类型都是实型。 935 ?两种类型都是整型。 936 ?一种类型是另一种类型的子界。 937 ?两种类型都是另一种宿主类型的子界。 938 ?两种类型都是另一种兼容基类型的集合类型。 939 ?两种类型都是紧凑字符串类型,并具有相同的元素个数。 940 ?一种类型是字符串类型,另一种类型是字符串类型、紧凑字符串类型或字符类型。 941 ?一种类型是Pointer类型,另一种类型是任意的指针类型。 942 ?两种类型都是类类型或,类引用类型,并且一种类型继承了另一种类型。 943 ?一种类型是PChar类型,另一种类型是形式为array[0..X] of Char的字符数组。 944 ?两种类型都是基类型相同的指针类型(编译开关$T设置为{$T+})。 945 ?两种类型都是结果类型相同、参数个数相同、参数类型一致的过程类型。 946 ?一种类型是Variant类型,另一种类型是整型、实型、字符串类型或布尔类型。 947 当两个类型要进行关系运算操作而又不满足类型兼容时,将产生编译错误。 948 6.4.2 赋值兼容 949 类型兼容仅仅可以进行关系运算,只有赋值兼容的变量才可以赋值或进行参数传递。 950 类型T2的值与类型T1的值赋值兼容是指T1和T2允许赋值操作,即: 951 T1:=T2; 952 Object Pascal规定,类型T1的值与类型T2的值赋值兼容必须有满足下列条件: 953 ?T1和 T2类型相同,并且都不是文件类型或包含文件类型的自定义类型。 954 ?T1是T2是兼容的有序类型,类型T2的值在类型类型T1的取值范围内。 955 ?T1和 T2都是实型, 类型T2的值在类型T1的取值范围内。 956 ?T1是实型,T2是整数型。 957 ?T1和 T2都是字符串类型。 958 ?T1是字符串类型,T2是字符类型。 959 ?T1是字符串类型,T2是紧凑的字符串类型。 960 ?T1是长字符串类型,T2是PChar类型。 961 ?T1和T2是兼容的、紧凑的字符串类型。 962 ?T1和T2是兼容的、集合类型。 类型T2的所有成员在类型T1的取值范围内。 963 ?类型T2在类型T1的取值范围内。 964 ?T1和T2是兼容的指针类型。 965 ?T1是类类型,T2是T1的继承类类型。 966 ?T1是类引用类型,T2是T1的继承类引用类型。 967 ?T1是PChar类型,T2是字符串常量。 968 ?T1是PChar类型,T2是形式为array[0..X] of Char的字符数组。 969 ?T1和T2是兼容的过程类型。 970 ?T1是过程类型,T2是具有与T1嗤�峁�嘈拖嗤�⒉问�鍪�嗤�⒉问�嘈鸵恢碌墓�袒蚝��?br> ?T1是Variant类型,T2是Integer,real,string或boolean类型。 971 ?T1是Integer,real,string或boolean类型,T2是Variant类型。当两个类型要进行赋值操作而又不满足赋值兼容时,将产生编译错误。 972 6.4.3 变量强制类型转换 973 变量强制类型转换就是强制将一种类型变量转换为另一种类型的变量。程序员自己确定强制类型转换的合法性。 974 图6.13是变量强制类型转换的语法规则。 975 当变量强制类型转换应用于一个变量时,该变量就被视为由类型标识符说明的类型。变量的大小必须与类型标识符说明的类型的大小相同。变量之前可以放置一个或多个类型允许的限定符。 976 Word类型的变量W转换为TByteRec, TWordRec(L)将一个LongInt类型的变量L转换为TWordRec类型,而PByte(L)则将LongInt类型变量L转换为指针类型Pbyte。 977 6.4.4 数值强制类型转换 978 数值强制类型转换就是强制将数值(或表达式)从一种类型转换为另一种类型。 979 其中表达式类型必须是有序类型或指针类型。 在转换中,如果结果类型的大小不同于表达式类型的大小,则有可能造成数据的截止或扩展。下面举例说明数值强制类型转换的用法。 980 Integer('A')//把字符A转换为一个整数。 981 Char(48)//把数字48转换为一个字符。 982 Boolean(0)//把数字0转换为一个布尔值。 983 Longint(@Buffer)//把指针转换为一长整数。 984 Int64(I)//把一个整数转换为64位整数 985 6.5 数据类型运算符 986 操作符 操作 操作数据类型 结果数据类型 987 DIV 整数除 integer integer 988 mod 余数除 integer integer 989 mod运算符的结果是两个操作数相除后的余数的整数部分。 990 shl 按位左移 integer Boolean 991 shr 按位右移 992 in 属于 993 6.5.4 运算符的优先级 994 运算符 优先级 分类 995 @, not 1 (最高) 一元运算符 996 *, /, div, mod, and, shl, shr, as 2 乘法运算符 997 +,-, or, xor 3 加法运算符 998 =, <>, <, >, <=, >=, in, is 4 (最低) 关系运算符 999 6.6 语 句 1000 这些例子都是DOS窗口方式的,而不是通常的Windows应用程序。如果读者要调试这些程序,需要修改Delphi的一些缺省设置。其步骤是: 1001 (1)开始Delphi。 1002 (2)如果当前不是自动打开一个新项目 ,选择File|New命令开始一个新项目 。 1003 (3)选择Project|Options|Linker命令,使能Generate Console Application核对框,从而是Delphi创建的程序是DOS窗口方式的,而不是通常的Windows应用程序。 1004 (4)选择View|Project Source命令,进入代码编辑器编辑项目 文件代码,键入本书提供的例子。 1005 (5)运行程序,程序将在它自己的DOS窗口运行。要关闭DOS窗口,选择Alt+F4或单击窗口的右上角单击X。 1006 6.6.1 赋值语句 1007 6.6.2 块语句 1008 1.ASM/END块语句 1009 ASM块语句在Pascal中嵌入汇编语言代码。由于Delphi Pascal对计算机资源提供了很好的支持,因此,除非特别需要,一般不需要使用汇编语句。 1010 2.BEGIN/END块语句 1011 6.6.3 Delphi控制语句 1012 Object Pascal使用控制语句来控制程序的执行顺序。7个是分支语句,3个是循环语句: 1013 .分支语句 1014 . if 语句 1015 . case语句 1016 CASE Choice of 1017 '1': EvaluateChoice; 1018 '2': Initialize 1019 ELSE Writeln('Bad Choice,Try Again.'); 1020 END; 1021 请注意,CASE语句的常量范围不能重叠。Else要放在所有判断语句之后. 1022 . GOTO语句 1023 GOTO语句强行将程序转向一个指定的点执行。该指定点用一个标号标识。 1024 . Break语句 1025 . Continue语句 1026 . Exit语句 1027 EXIT语句的功能是退出当前的代码块。如果代码块是主程序,EXIT语句导致程序的终止;如果当前块是嵌套的,EXIT语句跳到外一层嵌套继续执行。如果当前块是过程或函数,EXIT语句导致过程或函数执行终止,跳到调用过程或函数的语句的下一条语句执行。 1028 . Halt语句 1029 HALT语句导致程序的非正常结束,并返回到操作系统。通常是在程序遇到致命错误时才使用HALT语句。HALT语句后可跟一个整数代码HALT(1),以指定错误的原因。 1030 . 循环语句 1031 . Repeat/Until语句 1032 REPEAT 1033 Key:=GetChar; 1034 Writeln('Key IS',key); 1035 UNTIL Key=$D; 1036 . While语句 1037 WHILE key<>$D DO key:=GetChar; 1038 . for语句 1039 for V := Expr1 to Expr2 do Body; 可使用DOWNTO;在循环体中,如果不想执行循环下面的语句,而直接进入下一次循环,可以使用Continue语句. 1040 要退出循环,跳到FOR/DO语句下面的语句执行,可以使用break语句;如果不想执行循环下面的语句,而要求直接进入下一个循环,可以使Continue语句。 1041 6.7 过程与函数 1042 6.7.1 过程的定义和调用 1043 要定义和调用一个过程,首先要在程序的TYPE区声明一个过程. 1044 下面是一个过程声明的例子: 1045 procedure NumString(N: Integer; var S: string); 1046 过程声明之后,就应当在Implementation区定义这个过程,定义的规则如下。过程: 1047 Procedure <name>(<Parameters>) 1048 <declarations> 1049 BEGIN 1050 <statements> 1051 END; 1052 6.7.2 函数的定义和调用 1053 函数的定义和调用与过程的定义和调用类似,不同的是函数的首部,函数的首部多了一个返回结果类型。 1054 function Max(A: Vector; N: Integer): Extended; 1055 Max函数返回类型为Extended。函数声明之后,就应当在Implementation区定义这个函数。 1056 6.7.3 返回值 1057 (1)返回值直接送给函数名。 1058 (2)返回值送给Delphi的一个内置变量Result。 1059 如果你写的函数有可能移植到其它Pascal编译器中使用,最好使用第一种方式。 1060 6.7.4 调用约定 1061 从前面说明的过程和函数的语法规则我们知道,在声明过程或函数时,可以在附属块指定过程或函数的参数的传递方式。Pascal共提供了五种传递方式,分别为Register,Pascal,Cdecl,Stdcall,SafeCall。缺省的调用方式是Register方式。如果一个过程或函数没有指定过程或函数的调用方式,就采用Register调用方式。 1062 调用方式的语法示例如下: 1063 function Max(A: Vector; N: Integer): Extended;Stdcall; 1064 Object Pascal调用方式的区别于以下几点: 1065 (1)传递参数的顺序 1066 Register和 Pascal调用方式传递参数是从左到右,即最左边的参数先产生并首先传递,最右边的参数最后产生并最后传递。而Cdecl, Stdcall和 Safecall 调用方式传递参数则是从右到左。 1067 (2)堆栈中删除参数 1068 使用Pascal、Stdcall和Safecall调用方式的过程或函数在返回时程序自动删除堆栈中的参数,而Cdecl调用方式必须在程序返回时调用者自己删除堆栈中的参数。 1069 (3)使用寄存器传递参数 1070 Register调用方式使用三个CPU寄存器来传递参数,而其它调用方式使用堆栈来传递参数。 1071 Register调用方式通常是最快的参数传递方式,因为它不需要创建栈帧。Pascal和 Cdecl调用方式通常用于调用用C,C++或其它语言书写的动态链接库程序。Stdcall 调用方式通常用于Windows API程序。而Safecall调用方式通常用于实现OLE自动化编程的双接口(Dual interfaces)。 1072 6.7.5 指示字 1073 在声明过程或函数时,可以在附属块使用指示字以进一步指定过程或函数的产生方式。Delphi过程或函数分别提供了Block,External,Asm,Forward。指定调用方式的语法示例如下: 1074 procedure MoveWord(var Source, Dest; Count: Integer); external; 1075 其中Block是缺省方式,表示过程或函数的语句部分是 Pascal程序快,下面对External,Assembler,Forward进行介绍。 1076 1.External 1077 该指示字表示过程或函数是外部的,通常用于从动态链接库中引入过程或函数。External后可以动态链接库名或表示动态链接库的有序数,也可以指定引入的过程或函数名。例如: 1078 function MessageBox(HWnd: Integer; Text, Caption: PChar; Flags: Integer): Integer; stdcall; external 'user32.dll' name 'MessageBoxA'; 1079 上例中,user32.dll指定用于引入过程或函数的动态链接库名(也可以是一个有序数),MessageBox指定从动态链接库中引入过程或函数名。 1080 (2)Assembler 1081 该指示字表示过程或函数是使用嵌入式汇编语言编写的。例如函数声明: 1082 function LongMul(X, Y: Integer): Longint;Assembler 1083 其定义为: 1084 function LongMul(X, Y: Integer): Longint; 1085 (3)Forward 1086 该指示字表示一个过程或函数是向前查找的。在声明了一个过程或函数是向前查找的之后,该过程或函数的定义必须在后面的某个地方定义。 1087 procedure Walter(M, N: Integer); forward; 1088 procedure Clara(X, Y: Real); 1089 begin 1090 ... 1091 Walter(4, 5); 1092 end; 1093 procedure Walter; 1094 begin 1095 ... 1096 MessageBeep(0); 1097 end; 1098 注意:不能在单元的interface部分声明向前查找过程。在使用向前查找过程时,要注意相互递归。 1099 6.7.6 参数 1100 当调用过程或函数时,常常需要使用参数传递数据给被调用的过程或函数。在某种程度上,使过程、函数更有用更灵活的方法就是使用参数。 1101 在Pascal中,调用过程或函数使用的参数称为实参,被调用过程或函数使用的参数称为形参,例如,下面语句中,Edit1是实参: 1102 ColorIt(Edit1); 1103 下面的AnEditBox是形参: 1104 Procedure ColorIt(AnEditBox:Tedit); 1105 Delphi传递参数的方式有四种: 1106 (1)传值(Passing By Value)。 1107 变量和结构被完整地拷贝到堆栈中,而不是通过机器的寄存器。通过值传递参数可以防止调用的函数修改原来的参数,因为调用的函数接收到的只不过是参数的一个副本。例如: 1108 procedure Tform1.Button1Click(Sender:Tobject); 1109 var Number:Integer; 1110 begin 1111 Number:=StrToInt(Edit1.text); 1112 Calculate(Number); 1113 Edit2.Text:=IntToStr(Number); 1114 end; 1115 Procedure Calculate(CalcNo:Integer); 1116 begin 1117 CalcNo:=CalcNo*10; 1118 end; 1119 在Calculate过程中,CalcNo参数按值传递,执行该过程后,CalcNo的值扩大了十倍。但是,调用过程Tform1.Button1Click中Number并没有改变,因此Edit1编辑框与Edit2编辑框的值一样。 1120 (2)传引用(Passing By Reference)。 1121 传递一个指向参数的引用(指针),按规则引用可用作指针和值。改变引用传递的参数要影响调用源参数的拷贝。 1122 使用传引用必须在参数前加上Var保留字。例如,把Calculate改写如下: 1123 procedure Calculate(Var CalcNo:Integer); 1124 begin 1125 CalcNo:=CalcNo*10; 1126 end; 1127 修改后,Calculate过程的CalcNo参数为按引用传递,执行该过程后,CalcNo的值扩大了十倍,同时,调用过程Tform1.Button1Click中Number也作了改变,因此Edit2编辑框的值是Edit1编辑框值的10倍。 1128 (3)常量传递(Constant Parameters)。 1129 如果过程或函数运行时,形参的值永远都不会改变,就可以考虑使用常数参数。要使一个参数为常数参数,只要在参数前加上Const保留字。例如: 1130 function TDirectoryOutline.ForceCase(const AString: string): string; 1131 begin 1132 if Assigned(FCaseFunction) then 1133 Result := FCaseFunction(AString) 1134 else 1135 Result := AString; 1136 end; 1137 当你不需要参数改变时,可以使用常数参数防止偶然对该参数的修改。如果程序某个地方对常数参数进行了修改,你将会得到一个非法变量引用错误信息。 1138 (4)默认参数 1139 在Delphi中,可以为过程和函数定义默认参数。默认参数仅仅显示在参数列表的尾部,其形式是: 1140 参数名: 类型 = 值 1141 当调用包括默认参数的过程或函数时,默认参数的值可以省去。例如下面是一个函数的定义: 1142 procedure FillArray(A: array of Integer; Value: Integer = 0); 1143 下面是两个合法的调用语句: 1144 FillArray(MyArray, 1);//直接传递值 1145 FillArray(MyArray);//使用默认参数 1146 1147 1148 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lcq419/archive/2006/09/29/1305538.aspx