在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Object Passal的程序结构很特殊,与其它语言如C++,Object Windows等结构都不同。一个Delphi程序由多个称为单元的源代码模块组成。使用单元可以把一个大型程序分成多个逻辑相关的模块,并用来创建在不同程序中使用的程序库。 8.1 Program单元 Program单元就是Delphi中的项目文件。 Program单元是一个特殊的单元,类似于C语言中的Main程序,即为应用程序的主程序。一个程序可以有多个单元组成,也可以只有一个Program单元组成,例如前面我们介绍过的DOS窗口程序就只有一个Program单元组成。下面是一个典型的Program单元:program Project1; uses Forms, Unit1 in 'Unit1.pas' {Form1}; {$R *.RES} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end (1)程序首部指定程序名、以及参数等。 (2)Uses语句定义程序用到的所有单元。 标识符为单元的名字,各单元之间用逗好(,)隔开,最后结束用分号(;)。 注意:每个程序总是自动包含System单元,Program单元的Uses部分不能显式指定。System单元用于实现一些低级的运行时间程序的支持,如文件输入输出(I/O)、字符串操作、浮点运算、动态内存分配等。另外,Delphi在发行时提供了许多预先定义的单元,在构造程序时可以直接使用。例如,如果你将一个核对框放进一个窗体,你就自动地使用了StdCtrls单元,因为TCheckBox构件在StdCtrls中定义。 Uses部分列出单元的顺序决定它们初始化的顺序,并影响编译器定位标识符的顺序。如果两个单元定义了一个相同名字的类型,编译器将总是使用前面那个单元的类型。 (3)程序块由保留字Begin和End括起来的一段代码组成,用于对程序的初始化。 8.2 UNIT单元 UNIT单元相当于C语言的子程序。基本上Delphi每个窗体都一个对应的单元。当你为应用程序创建窗体时,你就创建了一个与该窗体相联系的新单元。然而,单元也可以独立于窗体而存在。例如,一个单元可以只包含数学运算程序,而不需要有窗体。 一个单元可以由多个程序共享。单元的磁盘文件名后缀为.pas。 8.2.1 单元结构 不管单元是否与窗体相关,单元的基本结构都是一样的 。UNIT单元由单元首部、接口部分(interface part)、实现部分(implementation part)、可选择的初始化部分(initialization part)、结束部分(finalization part)、end.组成。 8.2.2 单元首部 单元的首部用保留字Unit开始,后跟单元名。单元名必须遵循标识符的所有一般原则(不能以数字开头等)。下面的单元名将是有效的: Unit Rsgl; 8.2.3 接口部分(Interface) 在单元名之后是接口部分。接口部分用于声明变量、类型、过程和函数等。在接口部分声明的变量、类型以及过程、函数等是其它使用该单元的程序或单元等都可见的。接口部分用保留字Interface标明开始,用implemention标明结束。接口部分只能含有声明部分。 一个单元的接口部分还作为该单元说明文件的起点。虽然接口部分没有告诉你子程序做什么或变量如何使用,但它正确告诉了你的变量、类型、过程、函数等的名字及其调用方法。 接口部分本身又可以由几个可选的部分组成,分别是单元的USES语句、常量声明部分、类型声明部分、变量声明部分、过程和函数声明部分。其中常量声明、类型声明、变量声明、过程和函数声明部分用于声明其它使用该单元的单元可以访问的变量、类型、过程和函数等。 而USES语句列出该单元要用到的标准单元和其它单元,用于把外部的已定义的常量、类型、变量、过程或函数引入到本单元中使用。USES语句紧跟在Interface之后。 8.2.4 实现部分 单元的第二部分,称为实现部分(Implementation),主要用于定义接口部分声明过的过程、函数等的代码。实现部分用保留字implementation标明,总是紧随接口部分之后。 实现部分也可以用USES语句列出该单元要用到的标准单元和其它单元等。如上面的uses MDIEdit;语句。实际上,实现部分也可以声明变量、数据类型、过程及函数等。 但是,在实现部分定义的变量、类型、过程、函数等只能由本单元自己使用(private declarations),使用该单元的其它单元或程序不可见的。私有定义可以隐藏单元的细节。 8.2.5 USES子句 USES子句用于访问其它单元。例如,如果你要让程序来效仿一个电传打字机,可以在USES包含WinCRT,因为WinCrt含有进行这个仿效所需要的程序。 USES WinCRT; Delphi提供了许多预先定义的单元,你可以在程序中直接使用。实际上,当你将一个新构件放入设计的窗体时,DElphi会自动将该构件的单元放入USES子句中。例如,如果你将Color Grid放入窗体,则单元ColorGrd就附加在窗体单元的USES子句末尾,从而ColorGRd单元中接口部分所有定义都是窗体单元可以访问的。 要使用USES子句包含单元中的程序,只要在单元名后加上程序名即可。例如,如果要在Unit2中访问Unit1中的ComputePayMent函数。 USES子句可以放在接口部分(保留字Interface之后),也可放在实现部分(保留字Implementation之后),但是USES子句必须出现在它所有子程序、数据类型或变量被使用之前 。 USES子句放在实现部分可以隐藏单元的内部细节,同时可以避免循环引用发生。 8.2.6 初始化部分(Initialization) 初始化部分是单元的可选部分,主要用于对单元数据的初始化,一般很少用到。 该部分总是在其它程序代码之前运行。如果一个程序包含多个单元,则在程序的其它部分运行之前,每个单元的初始化代码都会先调用,其顺序是单元显示在Uses语句的顺序。 8.2.7 完成部分(Finalization) 完成部分(Finalization)是初始化过程的反过程,只要单元有初始化部分,才会有完成部分。完成部分对应在Delphi1.0中ExitProc和AddEXitProc函数。 完成部分在程序关闭时运行。任何在单元初始化时获得的资源如分配内存、打开文件等通常都在完成部分释放。单元完成部分的执行顺序与初始化部分相反。例如假如程序以A、B、C的顺序初始化单元,则完成过程的顺序是C、B、A。 一旦单元的初始化部分开始执行,就必须保证程序关闭时对应的完成部分执行。完成部分必须能够处理不完全初始的数据,因为如果产生异常,初始化代码可能不能完全执行。 1.构造 构造用建立对象,并对对象进行初始化。通常,当调用构造时,构造类似一个函数,返回一个新分配的并初始化了的类类型实例。 构造跟一般的方法不同的是,一般的方法只能在对象实例中引用,而构造既可以由一个对象实例引用,也可以直接由类来引用。当用类来引用类的构造时,实际上程序做了以下工作: (1)首先在堆中开辟一块区域用于存贮对象。 (2)然后对这块区域缺省初始化。初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为Nil,字符串类型的字段清为空等。 (3)执行构造中用户指定的动作。 (4)返回一个新分配的并初始化了的类类型实例。返回值的类型必须就是类的类型。 当你用在对象实例中引用类的构造时,构造类似一个普通的过程方法。这意味着一个新对象还没有被分配和初始化,调用构造不返回一个对象实例。相反,构造只对一个指定的对象实例操作,只执行用户在构造语句中指定的操作。 例如,在创建一个新的对象时,尽管还没有对象实例存在,仍然可以调用类的构造,程序示例如下: type TShape = class(TGraphicControl) private FPen: TPen; FBrush: TBrush; procedure PenChanged(Sender: TObject); procedure BrushChanged(Sender: TObject); public constructor Create(Owner: TComponent); override; destructor Destroy; override; ... end; constructor TShape.Create(Owner: TComponent); begin inherited Create(Owner);{ Initialize inherited parts } Width := 65;{ Change inherited properties } Height := 65; FPen := TPen.Create;{ Initialize new fields } FPen.OnChange := PenChanged; FBrush := TBrush.Create; FBrush.OnChange := BrushChanged; end; 构造的第一行是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上大多数构造都是这么写的。这句话的意思是首先调用祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派生类的字段,当然也可以重新对祖先类的字段赋值。用类来引用构造时,程序将自动做一些缺省的初始化工作,也就是说,对象在被创建时,其字段已经有了缺省的值。所有的字段都被缺省置为0(对于有序类型)、nil(指针或类类型)、空(字符串)、或者 Unassigned (变体类型)。除非想在创建对象时赋给这些字段其它值,否则在构造中除了Inherited Create(Owner)这句外,不需要写任何代码。 如果在用类来引用构造的过程中发生了异常,程序将自动调用析构来删除还没有完全创建好的对象实例。效果类似在构造中嵌入了一个try協inally语句,例如: try ...{ User defined actions } except{ On any exception } Destroy;{ Destroy unfinished object } raise;{ Re-raise exception } end; 构造也可以声明为虚拟的,当构造由类来引用时,虚拟的构造跟静态的构造没有什么区别。当构造由对象实例来引用时,构造就具有多态性。可以使用不同的构造来初始化对象实例。 2.析构 析构的作用跟构造正相反,它用于删除对象并指定删除对象时的动作,通常是释放对象所占用的堆和先前占用的其它资源。构造的定义中,第一句通常是调用祖先类的构造,而析构正相反,通常是最后一句调用祖先类的析构,程序示例如下: destructor TShape.Destroy; begin FBrush.Free; FPen.Free; inherited Destroy; end; 上例中,析构首先释放了刷子和笔的句然后调用祖先类的析构。 析构可以被声明为虚拟的,这样派生类就可以重载它的定义,甚至由多个析构的版本存在。事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是一个虚拟的无参数的析构,这样,所有的类都可以重载Destroy。 前面提到,当用类来引用构造时,如果发生运行期异常,程序将自动调用析构来删除还没有完全创建好的对象。由于构造将执行缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段释放为Nil。有一个比较稳妥的办法是,用Free来释放占用的资源而不是调用Destroy,例如上例中的FBrush.Free和FPen.Free,Free方法的实现是: procedure TObject.Free; begin if Self <> nil then Destroy; end; 也即Free方法在调用Destroy前会自动判断指针是否为Nil。如果改用FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产生异常导致程序中止。 7.2.3 方法指令字 声明方法的语法规则中,method directives为方法的指令字。 从语法示意图中可以看出,方法按指令字分又可分为三种,分别是虚拟、动态、消息方法,它们分别是方法名后用Virtual,Dynamic,Message保留字指定。也可以不加方法指令字,这种情况下声明的方法是静态的(static)。 另外,从语法示意图中可以看出,一个方法也可以像函数那样,指定参数的传递的方式,也即方法的调用约定。一个方法调用约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。 1.静态方法 缺省情况,所有的方法都是静态的,除非你为方法提供了其它指令字。静态方法类似于通常的过程和函数,编译器在编译时就已指定了输出该方法的对象实例。静态方法的主要优点是调用的速度快。 当从一个类派生一个类时,静态方法不会改变。如果你定义一个包含静态方法的类,然后派生一个新类,则被派生的类在同一地址共享基类的静态方法,也即你不能重载静态方法。如果你在派生类定义一个与祖先类相同名的静态方法,派生类的静态方法只是替换祖先类的静态方法。例如: type TFirstComponent = class(TComponent) procedure Move; procedure Flash; end; TSecondComponent = class(TFirstComponent) procedure Move;{该MOVE不同于祖先类的MOVE} function Flash(HowOften: Integer): Integer;{该Flash不同于祖先类的Flash} end; 上面代码中,第一个类定义了两个静态方法,第二个类定义了于祖先类同名的两个静态方法,第二个类的两个静态方法将替换第一个类的两个静态方法。 2.虚拟方法 虚拟方法比静态方法更灵活、更复杂。虚拟方法的地址不是在编译时确定的,而是程序在运行期根据调用这个虚拟方法的对象实例来决定的,这种方法又为滞后联编。 虚拟方法在对象虚拟方法表(VMT表)中占有一个索引号。 VMT表保存类类型的所有虚拟方法的地址。当你从一个类派生一个新类时,派生类创建它自己的VMT,该VMT包括了祖先类的VMT,同时加上自己定义的虚拟方法的地址虚拟方法可以在派生类中重新被定义,但祖先类中仍然可以被调用。例如: type TFirstComponent = class(TCustomControl) procedure Move;{ static method } procedure Flash; virtual;{ virtual method } procedure Beep; dynamic;{ dynamic virtual method } end; TSecondComponent = class(TFirstComponent) procedure Move;{ declares new method } procedure Flash; override;{ overrides inherited method } procedure Beep; override;{ overrides inherited method } end; 上例中,祖先类TFirstComponentw中方法Flash声明为虚拟的,派生类TSecondComponent重载了方法Flash。声明派生类的Flash 时,后面加了一个Override指令字,表示被声明的方法是重载基类中的同名的虚拟或动态方法。 注意:重载的方法必须与祖先类中被继承的方法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型一致。 要重载祖先类中的方法,必须使用Override批示字,如果不加这个指令字,而在派生类中声明了于祖先类同名的方法,则新声明的方法将隐藏被继承的方法。 3.动态方法 所谓动态方法,非常类似于虚拟方法,当把一个基类中的某个方法声明为动态方法时,派生类可以重载它,如上例的Beep。不同的是,被声明为动态的方法不是放在类的虚拟方法表中,而是由编译器给它一个索引号(一般不直接用到这个索引),当调用动态方法时,由索引号决定调用方法的哪个来具体实现。 从功能上讲,虚拟方法和动态方法几乎完全相同,只不过虚拟方法在调用速度上较快,但类型对象占用空间大,而动态方法在调用速度上稍慢而对象占用空间小。如果一个方法经常需要调用,或该方法的执行时间要求短,则在虚拟和动态之间还是选择使用虚拟为好。 4.消息句柄方法 在方法定义时加上一个message指令字,就可以定义一个消息句柄方法。消息句柄方法主要用于响应并处理某个特定的事件。 把一个方法声明为消息句柄的示例如下: type TTextBox = class(TCustomControl) private procedure WMChar(var Message: TWMChar); message WM_CHAR; ... end; 上例中声明了一个名叫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; begin Inherited ... end; 上例中,消息句柄首先调用WM_PAINT消息的缺省句柄,然后再执行自己的代码。使用Inherited保留字总是能自动找到对应于指定消息的缺省句柄(如果有的话)。 使用Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会自动调用TObject的DefaultHandler方法,这是个能对所有消息进行基本处理的缺省句柄。 7.2.4 抽象方法 从图7.7的方法指令字语法规则可知,可以在方法的调用约定之后加一个Abstract,以进一步指明该方法是否是抽象的。所谓抽象方法,首先必须是虚拟的或动态的,其次它只有声明而没有定义,只能在派生类中定义它(重载)。因此定义一个抽象方法,只是定义它的接口,而不定义底层的操作。 抽象方法在C++中称为纯虚函数,至少含有一个纯虚函数的类称为抽象类,抽象类不能建立对象实例。 声明一个抽象方法是用Abstract指令字,例如: type TFigure = class procedure Draw; virtual; abstract; ... end; 上例中声明了一个抽象方法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。在派生类中重载抽象方法,跟重载普通的虚拟或动态方法相似,不同的是在重载的方法定义中不能使用Inherited保留字,因为基类中抽象方法本来就没有定义。同样的道理,如果抽象方法没有被重载,程序不能调用这个抽象方法,否则会引起运行期异常。 7.2.5 重载方法与重定义方法 在子类中重载一个滞后联编的对象方法,需要使用保留字override。然而,值得注意的是,只有在祖先类中定义对象方法为虚拟后,才能进行重载。否则,对于静态对象方法,没有办法激活滞后联编,只有改变祖先类的代码。 规则非常简单:定义为静态的对象方法会在每个子类中保持静态,除非用一个同名的新虚拟方法隐藏它,被定义为虚拟的方法在每个子类中保持滞后联编。这是无法改变的,因为编译器会为滞后联编方法建立不同的代码。 为重新定义静态对象方法,用户只需向子类添加该对象方法,它的参数可以与原来方法的参数相同或不同,而不需要其它特殊的标志。重载虚拟方法,必须指定相同的参数并使用保留字override。例如: type AClass=Class procedure One;virtual; procedure Two;{static method} end; BClass=Clas(Aclass) procedure One;override; procedure Two; end; 重载对象方法有两种典型的方法。一种是用新版本替代祖先类的方法,另一种是向现有方法添加代码。这可以通过使用保留字inherited(继承)调用祖先类中相同的方法来实现。例如: procedure Bclass.One; begin //new code ...? //call inherited procedure Bclass inherited One; end; 在Delphi,对象可以有多个同名的方法,这些方法被称为重新定义的方法(overload), 并用保留字Overload标识。各同名的方法必须能够根据参数中不同的类型信息予以区分。例如: constructor Create(AOwner: TComponent); overload; override; constructor Create(AOwner: TComponent; Text: string); overload; 如果要重新定义一个虚拟的方法,在继承类中必须使用reintroduce指令字。例如: type T1 = class(TObject) procedure Test(I: Integer); overload; virtual; end; T2 = class(T1) procedure Test(S: string); reintroduce; overload; end; ... SomeObject := T2.Create; SomeObject.Test('Hello!'); // calls T2.Test SomeObject.Test(7); // calls T1.Test 在同一个类里,不同同时公布(publish)具有同名的重定义方法。例如: type TSomeClass = class published function Func(P: Integer): Integer; function Func(P: Boolean): Integer // error ... 7.3 类 的 特 性 特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的方法。特性可能是Delphi程序员接触得最多的名词之一,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗口的标题则修改Form的Caption特性,要改变窗口文字的字体则修改Form的Font特性。 Delphi的特性还有个显著特点就是,特性本身还可以是类类型,例如Font特性就是TFont类型的类。 7.3.1 声明特性 要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的方法。Object Pascal使用保留字Property声明特性。 特性的声明由保留字Property,特性标识符,可选的特性接口(Property Interface)和特性限定符(Property Specifier)构成。 特性接口指定特性的数据类型,参数和索引号。一个特性可以是除文件类型外的任何数据类型。 在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的方法。通常是把特性的值放在一个字段中,然后用Read和Write指定的方法去读或写字段的值。程序示例如下: type TYourComponent = class(TComponent) private FCount: Integer; { used for internal storage } procedure SetCount (Value: Integer); { write method } public property Count: Integer read FCount write SetCount; end; 上例中声明了一个TYourComponent类型的类,声明了一个字段FCount,它的数据类型是Integer,还声明了方法过程SetCount,最后声明了一个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取,用方法SetCount修改特性的值。 特性的声明似乎比较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,一个Integer型变量AInteger,程序可以这么写: AInteger:=Aobject.Count; Aobject.Count:=5; 实际上,编译器根据声明中的Read子句和Write子句自动把上述语句分别转换成: Ainteger:=Aobject.Fcount; Aobject.SetCount(5); 顺便说一下,跟访问字段和方法一样,要访问特性也需要加对象限定符,当然如果使用With语句则可简化。 跟字段不同的是,特性不能作为变量参数来传递,也不能用@来引用特性的地址。 7.3.2 特性限定符 特性限定符可以有四类,分别是Read,Write,Stored和Default。其中Read和Write限定符用于指定访问特性的方法或字段。 注意:Read和Write限定符指定的方法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后面介绍),这样能保证对特性的访问不会干扰到这些方法的实现,也能防止不小心破坏数据结构。熟悉C++的程序员可能已非常理解Private的含义,因为这正是面向对象的精髓之一。 1.Read限定符 Read限定符用于指定读取特性的方法或字段,通常是一个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如一个读取Caption特性的方法通常命名为GetCaption。 从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。 2.Write限定符 Write限定符用于指定修改特性的方法,通常是一个与特性同类型的过程,这个参数用于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的方法通常命名为SetCaption。 在Write限定符指定的方法的定义中,通常首先是把传递过来的值跟原先的值比较,如果两者不同,就把传递过来的特性值保存在一个字段中,然后再对特性的修改作出相应的反应。这样当下次读取特性值时,读取的总是最新的值。如果两者相同,那就什么也不需要干。 从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。只读的特性在Delphi中是常见的,只读的特性不能被修改。 3.Stored限定符 Stored限定符用于指定一个布尔表达式,通过这个布尔表达式的值来控制特性的存贮行为,注意,这个限定符只适用于非数组的特性(关于数组特性将在后面介绍)。 Stored限定符指定的布尔表达式可以是一个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。当表达式的值为False时,不把特性当前的值存到Form文件中(扩展名为DFM),如果表达式的值为True,就首先把特性的当前值跟Default限定符指定的缺省值(如果有的话)比较,如果相等,就不存贮,如果不等或者没有指定缺省值,就把特性的当前值存到Form文件中。 含有Stored限定符的特性声明示例如下: TSampleComponent = class(TComponent) protected function StoreIt: Boolean; public { normally not stored } property Important: Integer stored True;{ always stored } published { normally stored always } property Unimportant: Integer stored False;{ never stored } property Sometimes: Integer stored StoreIt;{ storage depends on function value } end; 上例中,TSampleComponent类类型包括三个特性,一个总是Stored,一个总是不Stored,第三个的Stored取决于布尔类型方法StoreIt的值。 4.Default和NoDefult限定符 Default限定符用于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有一个缺省值,例如把一个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过Default限定符设定的,程序示例如下 : TStatusBar = class(TPanel) public constructor Create(AOwner: TComponent); override; { override to set new default } published property Align default alBottom; { redeclare with new default value } end; ... constructor TStatusBar.Create(AOwner: TComponent); begin inherited Create(AOwner); { perform inherited initialization } Align := alBottom; { assign new default value for Align } end; 上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。 注意:Default限定符只适用于数据类型为有序类型或集合类型的特性,Default后必须跟一个常量,常量的类型必须与特性的类型一致。 如果特性声明时没有Default限定符(也可能是不能有Default限定符),表示特性没有缺省值,相当于用NoDefault限定符(NoDefault限定符只是强调一下特性没有缺省值,其效果跟什么也不写是一样的)。 7.3.3 数组特性 所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有一个索引号,不过跟一般的数组不同的是,一般的数组是自定义类型,可以把数组作为一个整体参与运算如赋值或传递等,而对数组特性来说,一次只能访问其中的一个元素。声明一个数组特性的程序示例如下: type TDemoComponent = class(TComponent) private function GetNumberName(Index: Integer): string; public property NumberName[Index: Integer]: string read GetNumberName; end; ... function TDemoComponent.GetNumberName(Index: Integer): string; begin Result := 'Unknown'; case Index of -MaxInt..-1: Result := 'Negative'; 0 : Result := 'Zero'; 1..100 : Result := 'Small'; 101..MaxInt: Result := 'Large'; end; end; 上例中,声明了一个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。上例中还同时声明了Read子句。从上面的例子中可以看出,声明一个数组特性的索引变量,跟声明一个过程或函数的参数类似,不同的是数组特性用方括号,而过程或函数用圆括号。索引变量可以有多个。 对于数组特性来说,可以使用Read和Write限定符,但Read和Write限定符只能指定方法而不能是字段,并且Object Pascal规定,Read限定符指定的方法必须是一个函数,函数的参数必须在数量和类型上与索引变量一一对应,其返回类型与数组特性的元素类型一致。Write限定符指定的方法必须是一个过程,其参数是索引变量再加上一个常量或数值参数,该参数的类型与数组特性的元素类型一致。 访问数组特性中的元素跟访问一般数组中的元素一样,也是用特性名加索引号。 7.3.4 特性重载 所谓特性重载,就是在祖先类中声明的特性,可以在派生类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后面详细介绍),重新指定访问方法和存贮限定符以及缺省限定符等。 最简单的重载,就是在派生类中这么写: Property 特性名: 这种重载通常用于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。 特性重载的原则是,派生类中只能改变或增加限定符,但不能删除限定符,请看下面的程序示例: type TBase = class ... protected property Size: Integer read FSize; property Text: string read GetText write SetText; property Color: TColor read FColor write SetColor stored False; ... end; type TDerived = class(TBase) ... protected property Size write SetSize; published property Text; property Color stored True default clBlue; ... end; 对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,首先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了一个Default限定符。 7.4 类成员的可见性 面向对象编程的重要特征之一就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么几个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。如 TBASE = class private FMinValue: Longint; FMaxValue: Longint; procedure SetMinValue(Value: Longint); procedure SetMaxValue(Value: Longint); function GetPercentDone: Longint; protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; procedure AddProgress(Value: Longint); property PercentDone: Longint read GetPercentDone; published 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 end; 上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表示它们是私有的,Public部分声明的几个方法是公共的。 再请看下面的例子: TBASE = class FMinValue: Longint; FMaxValue: Longint; private procedure SetMinValue(Value: Longint); procedure SetMaxValue(Value: Longint); function GetPercentDone: Longint; protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; procedure AddProgress(Value: Longint); property PercentDone: Longint read GetPercentDone; published 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; end; 上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型首部,前面没有任何描述可见性的保留字,那么它们属于哪一类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是用{$M+}状态编译的基类,其可见性为为Published,否则就是Public。 7.4.1 Private 在Private部分声明的成员是私有的,它们只能被同一个类中的方法访问,相当于C语言中的内部变量,对于其它类包括它的派生类,Private部分声明的成员是不可见的,这就是面向对象编程中的数据保护机制,程序员不必知道类实现的细节,只需要关心类的接口部分。 7.4.2 Public 在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。但其它类的实例也可以引用,相当于C语言中的外部变量,例如,假设应用程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量Count,则可以把Count放在TForm1类中的Public部分声明,然后把Unit1加到Init2的Interface部分就可以了。 注意:面向对象的编程思想其特征之一就是隐藏复杂性,除非必须把某个成员在不同类之间共享,一般来说尽量不要把成员声明在类的Public部分,以防止程序意外地不正确地修改了数据。 7.4.3 Published 在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是一样的,它们都是公共的,即这些成员可以被其它类的实例引用,Published和Public的区别在于成员的运行期类型信息不同。一个Published元素或对象方法不但能在运行时,而且能在设计时使用。事实上,Delphi构件板上的每个构件都有Published接口,该接口被一些Delphi工具使用,例如Object Inspector。 注意:只有当编译开关$N的状态为$M+时或者基类是用$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M用于控制运行期类型信息的生成。 7.4.4 Protected Protected与Private有些类似。在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派生类中可见的,并且成为派生类中的私有成员。 在Protected部分声明的成员通常是方法,这样既可以在派生类中访问这些方法,又不必知道方法实现的细节。 7.4.5 Automated 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不能重复。 7.5 类类型的兼容性 一个类类型类与它的任何祖先类型兼容。因此,在程序执行时,一个类类型变量既可以引用那个类型本身的实例,也可以引用任何继承类的实例。例如下面的一段代码: type TScreenThing = Class X,Y:Longint; procedure Draw; end; T3DScreenThing = Class(TScreenThing) Z:Longint; end; procedure ResetScreenThing(T:TScreenThing); begin T. X:=0; U. Y:=0; V. Draw; end; procedure Reset3DScreenThing(T:T3DScreenThing); begin T. X:=0; T.Y:=0; T.Z:=0; U.Draw; end; var Q:TScreenThing; R:T3DScreenThin; begin {...} ResetScreenThing(Q); ResetScreenThing(R); {this work} Reset3DScreenThing(Q); { but this does not} 在上面,过程ResetScreenThing定义时使用TScreenThing类型的参数,但可以使用TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。而Reset3DScreenThing使用TScreenThing类型的参数就非法。 7.6 VCL类结构 我们介绍过的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的取值范围及存储格式。 表6-2 一般整数类型的取值范围及存储格式 数据类型 取值范围 存储格式 Integer ?147483648..2147483647 32位有符号 Cardina l0..4294967295 32位无符号 基本整数类型包括Shortint、Smallint、Longint、Int64、Byte、Word和Longword。表6-3列出了它们的取值范围及存储格式。 表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; ... I := High(Integer); J := I + 1; 在这种情况下,要取得一个Int64的值,必须进行类型转换: J := Int64(I) + 1; 注意:绝大多数例程在遇到Int64时都把它转换为32位。但例程High,Low,Succ,Pred,Inc,Dec,IntToStr和IntToHex则完全支持Int64参数。Round,Trunc,StrToInt64,和StrToInt64Def函数可以返回Int64类型的结果。 (2)字符类型(Char) 字符类型中Char类型设计来只存储一个字符。一个字符占一个字节,因此Char数据类型可以存储256个不同的字符,其对应的整数为0到255。 除了Char数据类型外,Delphi还提供了Char类型的扩展,即AnsiChar和WideChar型。表6-4是字符数据类型的列表。 表6-4 字符整数类型 字符类型 占用字节数 存储内容 AnsiChar 1 存储一个Ansi字符。 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()函数,以确定字符占多少字节。 (3)布尔类型(Boolean) Boolean数据类型的变量只存储一个逻辑值,例如True或False。共有4种Boolean数据类型,见表6-5。 表6-5 布尔类型 类型 说明 Boolean 占1个字节 ByteBool 占1个字节 WordBool 占2个字节 LongBool 占4个字节 Delphi提供多种Boolean数据类型的目的是为了兼容,因为在某些情况下,Windows需要用一个字(2个字节)或双字(4个字节)来表示一个布尔值。 (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,通过赋整数值来代表星期几,也可以得到同样的结果。但是,由于枚举类型表达的意思明确、直观、便于记忆,因此使用枚举类型仍有必要。 下面是声明一个枚举类型的语法(图6.2)。 其中标识符列表中的标识符之间用逗号隔开,它列出该类型可能具有的值。 下面是声明一个枚举类型变量的举例: var DayOfWeek:TDays; Hue:TPrimaryColor; Department:TDepartment; Dogbreed:TDog; 也可以把类型声明和变量声明合二为一,例如: 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); 两个类型变量中都有Green元素,是不允许的。 3)不能直接用枚举类型中的元素参加运算,例如,下面的语句就是错误的: X:=Red+Blue; 但是,可以用某个枚举类型中的元素对枚举变量赋值,例如,下面的语句: DayOfWeek:=Monday; (5)子界型 子界类型是Integer,Boolean,Char及枚举型等称为宿主类型数据的一个子集。当你要限制一个变量的数据范围时,使用子界类型就特别有用。子界类型也是一种用户自定义的简单数据类型。要定义子界类型,必须说明区间的最大值和最小值,下面是子界类型定义的一些例子: type TCompassRange = 0..360; TValidLetter ='A'..'F' TMonthlyIncome=10000..30000; THours =0..23; TPrecipitation=(Drizzle,Showers,Rain,Downpour,Thunderstorm); {枚举型} TRain =Drizzle..Downpour; {上面枚举型的子界型} 下面是声明一个子界类型的语法规则(图6.3)。 其中两个常数(称为上界和下界)必须是同一种有序类型,如Integer,Boolean,Char及枚举型等,但不能是Real数据类型。第一个常数必须小于或等于第二个常数。 下面是声明子界类型变量的举例: var Compass:TCompassRange; ValidChar:TValidLetter; 在声明子界类型和子界类型变量时,请注意以下几点: 1)上界常数和下界常数必须是同一类型,且都是有序类型。 2)子界类型变量具有宿主类型数据的所有运算特性,但运算的结果必须在范围内。 3)上界常数和下界常数可以是表达式。例如: const X = 10; Y = 50; type Color = (Red, Green, Blue); Scale = X * 2..X * 20; 2.实数类型(Real) 实数类型是带有小数部分的数值,存储实数。有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 当前通常使用的Real等价与Double。 6.3.2 字符串类型 Delphi在处理字符串时,提供了多种方式,表6-7是Delphi使用的字符串类型。 表6-7 字符串类型 类型 最大长度 存储的字符 是否以Null结尾 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类型。例如,如果你像下面那样定义字符串: var S: String;// S is an AnsiString 则编译器假定你要创建一个AnsiString数据类型变量。 使用$H编译命令可以改变缺省定义。当在程序中把编译开关$H的状态改为{H-}时,String缺省是ShortString类型;当在程序中把编译开关$H的状态改为{H+}时,String缺省是AnsiString类型。例如: var {$H-} S1: String; // S1 is a ShortString {$H+} S2: String; // S2 is an AnsiString 如果定义中指明了长度(最大为25 5),则String为ShortString。例如: var S: String[63]; // S是一个 长度为63的ShortString变量。 6.3.3 结构数据类型 结构类型在内存中存储一组相关的数据项,而不是像简单数据类型那样单一的数值。Object Pascal结构类型包括集合类型、数组类型、记录类型、文件类型、类类型、类引用类型、接口类型等。这里,我们只介绍集合类型、数组类型、记录类型和文件类型。类类型、类引用类型和接口类型放在下一章介绍。 1.数组(Array) 数组是一种数据类型数据的有序集合,是代表一定数量具有相同类型变量的一种数据类型。Object Pascal数组可与任何简单数据类型或字符串类型等一起使用。数组可用于声明一个简单变量或作为一个记录类型定义的组成部分。 (1)数组的定义 下面是声明一个数组类型的语法规则(图6.4)。 要声明一个数组变量,要求你提供一个标识符,使用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; 访问数组中的元素很简单,只要在标识符后面的方括号中给出指定的元素的索引号即可。例如: Check[5]:=12; J:=9; Check[J]:=24; 要访问数组中的所有元素,可以使用循环语句。例如 : For J:=1 to 10 do Check[J]:=0; (2)多维数组 上面介绍的是一维数组。实际上,数组可以是多维的。例如,如果你想编写一个数组来容纳一张电子表格中的值,那么就可以使用2维数组。下面的例子说明如何使用2维数组定义一个有20行和20列的表格: Type Ttable = array[1..20,1..20] of Double; Var BigTable:Ttable; 要将2维数组中的所有数据初始化,可以使用如下语句: Var Col,Row:Intger; . . . for Col:=1 to 20 do for Row:=1 to 20 do BigTable[Col,Row]:=0.0; 使用多维数组时,要记住的一件事是数组为每维所占据的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个字节 (3)字符数组 前面介绍的字符串,实际上就是一个1维字符数组,只不过Pascal对字符串类型作了特殊的准许,你可以把它看作一个整体。字符串类型本质上等同于下列类型:type StringType:array[0..255] of char;但是,虽然你可以把一个字符串看待,但它仍然保持其数组的特性。例如在定义一个字符串类型变量时,你可以说明字符串的大小,就像你定义字符数组的大小一样。下面是几个字符串类型定义: type MyString:string[15]; BigString:string; LittleString:string[1]; 上面语句定义MyString类型包含15个字符,LittleString包含1个字符,BigString没有说明大小,就取字符串包含字符的最大个数255。然后你可以定义这些类型的变量,就像使用其它类型一样: Var MyName:MyString; 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]); (4)数组类型常量 数组类型常量的每个字段都是类型常量,下面是声明数组类型常量的语法规则(图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))); Maze常量数组各元素的值为: Maze[0, 0, 0] = 0 Maze[0, 0, 1] = 1 Maze[0, 1, 0] = 2 Maze[0, 1, 1] = 3 Maze[1, 0, 0] = 4 Maze[1, 0, 1] = 5 Maze[1, 1, 0] = 6 Maze[1, 1, 1] = 7 (5)开放式数组 所谓开放式数组,是指数组作为形参传递给过程或函数时其长度是可变的,这样在调用过程或函数时,可以传递不同长度的数组作为实际参数。 开放式数组在过程或函数中作为形参可以定义为: 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; {Clear过程对一个Double数组的各元素清0,SUM过程计算一个Double数组的各元素之和。两个过程的参数都是开放式数组。} procedure Clear(var A: array of Double); var I: Integer; begin for I := 0 to High(A) do A[I] := 0; end; function Sum(const A: array of Double): Double; var I: Integer; S: Double; begin S := 0; for I := 0 to High(A) do S := S + A[I]; Sum := S; end; begin Clear(X1); Clear(X2); Sum(X1); Sum(X2); end; 当开放式数组的元素类型为Char时,实参可以是一个字符串常数。例如: procedure PrintStr(const S: array of Char); var I: Integer; begin for I := 0 to High(S) do if S[I] <> #0 then Write(S[I]) else Break; end; 下面是合法的过程调用语句: PrintStr('Hello world'); PrintStr('A'); (6)动态数组 在Delphi中,除了定义静态数组外,还可以定义动态数组。动态数组只需说明数组的类型信息(包括数组的维数和数组元数的类型),但不需要定义元素的个数。例如: A: array[1..100] of string;//静态数组 B: array of integer//动态数组 C: array of array of string;//动态数组 这里A是静态数组,B是一维的整数动态数组,C是二维的字符串动态数组。 动态数组没有固定的长度。相反,当为动态数组赋值或使用SetLength过程时,动态数组的内存空间将重新分配。动态数组的定义形式是: array of baseType 例如: var MyFlexibleArray: array of Real; 定义了一个类型为实数型的一维动态数组。注意,声明语句并没有为MyFlexibleArray分配内存。要为动态数组分配内存,需要调用SetLength过程。例如: SetLength(MyFlexibleArray, 20);上面语句分配20个实数,标号从0到19。 动态数组的标号是整数类型,标号总是从0开始。使用Length,High和Low函数可以取得有关动态数组的特性。Length函数返回数组中元素的个数。High函数返回数组的最大标号,Low返回0。 2.集合类型 集合类型是Integer,Boolean,Char,枚举型,子界型等类型数据的一个子集。在应用程序中,当要检测一个数是否属于一个特定的集合时,就可以使用集合类型。(1)集合类型的定义下面是声明一个集合类型的语法规则(图6.6)。 其中Set of是保留字,ordinal Type是集合的基类型,可以是任何有序类型如整数型,布尔型,字符型,枚举型和子界型,但不能是实型或其它自定义类型。下面是一些集合类型的例子: type VoterDataSet= Set Of (Democrat,Republican,Male,Female, LowOpinion,HighOption,Confused); Chars = Set of Char; 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。 下面是集合类型变量的一些例子: var Voter: VoterDataSet; Color: ColorSet; Lets:Letters; Pascal使用方括号来表示集合,例如: [Democrat];表示只含Democrat的集合。 一个集合可以拥有0个元素,这时称之为空集,用两个方括号表示,其中什么也没有。对于集合类型变量,你可以进行+,-,=,*(并),IN等运算。见下表6-8。 表6-8 集合类型运算 操作符 描述 举例 + 往一个集合中添加元素 Aset:=Aset+AnotherSet; - 从一个集合中去除元素 Aset:=Aset-AnotherSet; * 去除两个集合中都没有的元素 Aset:=Aset*AnotherSet; In 测试元素 Bool:=AnElement in Aset 下面是集合运算的例子: Voter:=[HighOption]; Voter:=Voter+[Democrat]; Voter:=Voter+{male}; Voter:=Voter-[HighOption]; If HighOption in Voter then SendtheVoterFlowers; (2)集合类型 常量像简单类型常量一样,集合类型常量用来定义一组常量的集合。例如: 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的一个子集。 3.记录类型 记录是一系列相关的变量,这些变量被称为域,它们放在一起,作为一个整体使用。例如,一个雇员可能包含姓名、雇用时间、薪金等数据,这时你可以像下面那样定义一个雇员记录类型: type TEmployee = record LastName: String[20]; FirstName:String[15]; YearHired:1990..2000; Salary:Double; Position:string[20]; end; Pascal的记录类型跟数据库中 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论