delphi 语法3

简单类型共包括有六种类型:整型、字符型、布尔型、枚举类型、子界类型、实型。 除实型之外,其它五种类中每一种类型的值域(所有合法的值的集合)中值的数目有限

且其中的值排列有序,其每个值在值域中均有一个序数 n,其前后两个值的序数分别为 n-1 和 n+1。这样的数据类型称之为顺序类型(ordinal type),也有些书上翻译成有序类型。

      1. 整型类型

整型类型是由数学中的所有整数(包括正整数、负整数和 0)所组成的集合的一个子 集。如其中的 byte 类型表示从 0 至 255 共计 256 个整数;shortint 表示-128 至 127 共

256 个整数。

Delphi2010 中的整型类型包括 Integer、Cardinal、Shortint、Smallint、Longint、 Int64、Byte、 Word、Longword 以及 UInt64 共 10 种具体类型。其中最为常用的是 Integer 和 Cardinal,此二者为一般整型类型,分别等同于 longint 及 longword 类型,其 性能经过特别优化处理,在多数环境下能获得较好的表现。另外六种为基本整型类型,一 般不常用。下表给出上面 10 种类型各自的取值范围:

|----------|-------------------------|-----------------|
| 类型名称 | 取值范围 | 存储格式 |
| Integer | -2147483648..2147483647 | signed 32-bit |
| Cardinal | 0..4294967295 | unsigned 32-bit |
| |||
| Shortint | -128..127 | signed 8-bit |
| Smallint | -32768..32767 | signed 16-bit |
| Longint | -2147483648..2147483647 | signed 32-bit |
| Int64 | -2^63..2^63-1 | signed 64-bit |
| Byte | 0..255 | unsigned 8-bit |
| Word | 0..65535 | unsigned 16-bit |
| Longword | 0..4294967295 | unsigned 32-bit |
| UInt64 | 0..2^64-1 | unsigned 64-bit |

注 \]:上表中,signed 表示有符号,表明此类型可取正数也可取负数。Unsigned 表示无 符号,表示此类型只能取正数和 0。如 shortint 存储格式为 signed 8-bit,表示此类型为 8 位有符号数,其在内在中占据个字节,所能表示的最大值为 =256,但此类型为有符号 格式,故其取值为-128 至 127(包括 0)。 1. 1. 1. 实型类型 类似于整型,实型为所有实数所组成的集合的子集。其各种具体类型及取值范围如下 表: |--------|---------------|----|-----|---|--------|-------|-------| | 类型名称 | 取值范围 ||||| 有效位数 | 所占字节数 | | Real48 | 2.9 x 10\^-39 | .. | 1.7 | x | 10\^38 | 11-12 | 6 | | Single | 1.5 x 10\^-45 | .. | 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 | 10-20 | 10 | | Comp | -2\^63+1 .. 2\^63-1 | 10-20 | 8 | | Currency | -922337203685477.5808 ... 922337203685477.5807 | 10-20 | 8 | | Real | -5.0 x 10\^-324 ... 1.7 x 10\^308 | 15-16 | 8 | \[ 注 \]:在早期的 Delphi 某些版本中,Real 等同于上表中的 Real48 类型。为了保持兼容 性,用 Delphi2010 编译相应早期版本所编写的代码时,须手动改正,或者在 Delphi2010 中使用{$REALCOMPATIBILITY ON}编译指令使 Real 等同于 Real48; 类似于整型,实型亦有一般类型与基本类型之分,上表中的 Real 为一般类型,其余均 为基本类型。但 Double 类型在实现上完全等同于 Real 类型。多数情况下,使用 Real 可获 得最好的性能。 关于基本类型,有以下几点须注意: 1. Real48 是为兼容以前的代码而保留,因其不能于 Intel CPU 平台下优化,故运行稍 慢。不推荐使用。 2. Extended 精度较高,但编写跨平台共享文件时须小心使用。 3. Comp 为 Intel CPU 的原生 64 位整数,之所以被归类为实型,是因为相比整型言,其内 部实现类似于实数,例如此类型不能进行递增或递减运算。使用此类型时建议以 Int64 代替提高性能。 4. 任何计算结果若是 Currency 类型,不管这个结果原本有多少位小数,都将自动保留四 位小数。 1. 1. 1. 字符类型 字符类型用于描述一个单独的书面的文字和符号。Delphi 支持 AnsiChar 与 WideChar 两种基本字符类型。AnsiChar 类型变量使用单字节来表示一个字符,WideChar 则使用两个 字节来表示一个字符。WideChar 与 AnsiChar 类型的变量之间不能相互赋值: var wch:WideChar; ach:AnsiChar; begin ach := '国'; //错误。ach 占用一个字节而汉字占两个字节 ach := wch; //错误 end. Delphi 中最常用的字符类型是 Char 类型。它是上面两种字符类型其中一种的别名。 在 Delphi2010 中 Char 被默认为 WideChar,但在之前的版本如 Delphi7 中,此类型代表 AnsiChar。 Delphi 中的字符与字符串没有明显区别,编译器将所有的只含有一个字符(对于 AnsiChar 则是一个字节)的字符串如'A'、'B'当成一个字符。 1. 1. 1. 布尔类型 布尔型又称之为逻辑型,用于判断真假,其值只有 true 和 false 两种类型。Delphi 中有四种内置布尔类型:Boolean,ByteBool,WordBool,LongBool。 Boolean 是 Delphi 中最为常用的布尔类型,此类型变量在内存中占据 1 个字节,其值 域中只有 true 和 false 两个枚举常量值。其中 true 的序数值为 1,false 的序数值为 0。 将其它类型的值赋给布尔型变量时,必须显式进行类型转化,非 0 值将被转化为 true,而 0 被转化为 false。例如: begin if boolean(0) then writeln('False'); //不显示 if boolean(3) then writeln('True'); //显示 True end. ByteBool,WordBool,LongBool 在内存中分别占据 1,2,4 个字节。这三种类型中, false 的序数为 0,但与 boolean 不同,true 的序数并非为 1,包括正数和负数在内的所有 非 0 的值均被视为 true。 1. 1. 1. 枚举类型 枚举类型表示一个有次序且数量有限的值的集合,其中的值用标识符表示。 1. 枚举类型的声明 枚举类型的声明格式如下: Type 类型名=(标识符 1,标识符 2...标识符 n); Type 为 Delphi 的保留字,用于声明一个新的数据类型。其后的(标识符,标识符 2...标识 符 n)称为枚举类型的值域,表示此类型变量可取值的集合。 枚举类型中的每个标识符均具有一定的序数,在未替任何标识符指定序数的情形下, 编译器将按照先后顺序从 0 开始依次给每个成员分配序数。如三原色用枚举表示为: Type BasicColor = (red,green,blue); 此语句声明了一个枚举类型 BasicColor,其包含三个成员 red、green 及 blue,三者 序数分别为 0,1,2,此类型的变量只能取这三个值的其中之一。 枚举类型允许使用成员的序数表示成员,其形式为: enum(n) 上式中,enum 表示一个枚举类型名称,n 表示任一成员的序数,enum(n)表示枚举类型 enum 中序数为 n 的成员。如 BasicColor(0)、BasicColor(1)、BasicColor(2)分别表示 red、green、blue。 用户在声明枚举类型时可以自己指定枚举成员的序数,对于未指定序数的成员,默认 其序数为前一个成员的序数加上 1。若第一个成员未指定序数,将会默认为 0。据此,如下 声明: Type Myenum4 = (i1,i2,i3 = 4,i4,i5 = 8); 其中 i1 至 i5 的序数分别为:0,1,4,5,8。从表面看来,myenum4 有五个成员,分 别是:i1,i2,i3,i4,i5。但事实情况是,Myenum4 共包括 9 个而非 5 个值。 造成上述情形是因为枚举类型的成员个数不是由此枚举类型中的标识符的数量决定, 而由其标识符中最大序数与最小序数决定。如:Myenum4 中最大与最小序数分别为 8、0, 故其有 9 个成员,只不过其它 4 个成员并未在声明时用标识符表示。使用未使用标识符标 识的成员时,可利用序数索引表示,如 Myenum(6)表示其第 7 个成员,Myenum(4)表示其第 5 个成员。(注意序数是从 0 开始) 枚举类型的每个成员均为一个直接常量,就像英文字母 ABC 一样,它们不代表其它任 何值,因其本身正是一个确定值。 在 Delphi 中枚举类型的每个成员的标识符均被理解成是一个符号常量。例如: Type Myenum = (i1,i2,i3); 这个声明相当于定义了三个符号常量: Const i1 = 0; i1 = 1; i2 = 2; 若再次将这些标识符声明用于其它声明,编译器会发现同一个标识符被用于了多个声 明语句之中,会提示错误。如: Var i1:string; 标识符 i1 既在 myenum 中被声明为符号常量,又于其后被声明为一个 string 类型的变量, 所以此声明不能通过编译,编译器会提示错误:标识符 i1 不能被重新使用。 2. 枚举类型变量的声明 枚举类型的变量可以使用的枚举类型名称来声明,也可以直接使用枚举类型的值域声 明。下面两个变量的声明均属合法: Type Myenum = (i1,i2,i3); Var v1:myenum; v2:(a,b,c); 当利用了某一枚举类型的名称声明一个变量后,在此变量有效的范围之内不能使用其 枚举类型的本体声明其它变量。如上面的声明中,若 v2 的声明为 V2:(i1,i2,i3); 编译器将会提示错误:标识符被重新定义。因为枚举类型可以有未指定名称的成员, 同理,亦可以存在未指定名称的枚举类型,上面 v2 的类型(i1,i2,i3)就是一个未指定名称 的类型,而其中的标识符与 Myenum 中的标识符完全一样,这相当于同时定义了两种标识符 相同的枚举类型,故而引起了名称的冲突。 - 39 - 1. 1. 1. 子界类型 所谓子界,子为子集,界为界限。子界类型就是一种利用上界与下界在某种其它有序 类型的值域中划定一个范围作为自己的值域的类型。例如 byte 可视作编译器内置的子界类 型,它在 integer 类型的值域中插入了 0 与 255 两个边界,则其值域为 0 至 255 共计 256 个值。 1. 子界类型的声明 同枚举一样,子界类型的声明以 type 保留字作为开始: Type 子界类型名=下界..上界 其中的上界与下界必须为其它有序类型中的直接常量,如整型数字、枚举成员等。上 界及下界所属的类型称之为此子界类型的基类型。注意上界的序数不能小于下界的序数。 例如:若有枚举类型 myenum = (i1,i2,i3,i4,i5);可以定义子界类型: Type Mysub = i3..i5; 则此类型的取值值域为 i3,i4,i5,基类型为 myenum。 类似于枚举类型,子界类型也可以通过其上下界直接声明变量,如: var Mycap:'a'..'z'; Mycap 表示一个子界类型的变量,其域为从'a'至'z'的 26 个小写英文字母,基类型为 字符型。 \[ 注 \]:在用 type 声明数据类型时,若'='后第一个符号为'(',编译器将自动 将此声明当成是枚举类型的声明。据此,若声明如下的子界类型: Type Mysub = (2+3)\*2..(6+4)\*4; 由于'='后第一个符号为'(',编译器会认为这是在声明一个枚举类型,故而给 出一个错误。 2. 子界类型变量 声明子界类型变量有两种途径:其一是用类型名称来声明。其二是用下界..上界的形 式直接声明。下面的例子中,v1 及 v2 的声明均为合法声明: Type Myenum = 0..10; Var V1:myenum; V2:1..10; 与枚举类型不同,V1 与 V2 并不会引起编译报错,可以认为 V1 与 V2 等效。因为子界 类型只是在已有数据类型中截取一部分值域,并没有声明除类型名称以外的任何标识符, 故而不存在标识符冲突的问题。 - 40 - 子界类型仅是在基类型的值域中加上上下界从而限定了子界类型的变量在基类型的值 域中可取的值的范围而已,但其本质与基类型完全相同。 读者可如此理解:一滴水与一碗水之间除了份量有区别之外,其它完全一致。 替子界类型的变量赋值时,若所赋之值不在子界类型的值域内,则会提示错误,如对 于上面声明的变量 v1,下面的赋值在编译器的默认设置下将不会通过编译: V1 := 15; ### 3.2 结构类型 ##### 3.2.1 集合及其运算 1. 集合类型的声明 集合类型表示某个有序类型的若干个值的集合。可以将集合类型理解为一个用于容纳 数据的容器,只是其容纳的数据必须为有序类型。声明一个集合类型的方式如下: Type Myset = set of BaseType; Myset 为所声明的集合类型的名称。BaseType 为集合中所含成员的类型,称之为基类型。 Delphi 规定: l 一个集合类型的所有成员必须是同一有序类型且最多只能含 256 个成员; l 集合类型成员具有唯一性,即同一集合类型中不允许含有相同的成员; l 集合类型成员具有无序性,其中的成员没序数。无法像枚举型一样用序数表示集 合成员; l 集合类型的值域由 BaseType 的值域决定。 以下给出一些实例: Type Set1 = set of byte; //set1 的成员为 byte 型,值域为 0 至 255 共 255 个整 型值 Set2 = set of 1..9; //set2 的成员为子界类型,值域为 1 至 9 共 9 个整型 值 Set3 = set of (red,green,blue); //set3 的成员类型为枚举类型,值域为 3 个标识符 2. 集合类型变量的声明与赋值 声明集合类型的变量有两种方式:可使用集合类型的名称来声明,也直接利用集合类 型的本体来声明: Var Vset1:set1; Vset2:set of byte; 给集合类型的变量赋值时,所赋之值应使用方括号括起,括号内的成员以逗号隔开。 如: - 41 - Vset1 = \[1,3,5\]; 集合变量 Vset1 中所含的成员有三个,分别为:1、3、5。 1. 1. 1. 数组 数组是由若干个同类型且具有序号的变量(即数组成员)所组成的队列。与普通变量不 同,数组成员没有变量名,只能通过数组名和序号组成的索引来存取。如 A\[2\]、C\[4\]等。 在分配内存时,数组的所有成员将被安排在一段连续的区域中。 Delphi 的数组可分为静态数组和动态数组两种。 1. 静态数组 静态数组是在程序初始化时就被分配内存的数组。鉴于此,静态数组在定义之后其大 小不能更改。定义一个 N 维静态数组的方法如下: Type TypeName = Array \[IndexTypeType1,IndexType2...IndexTypeN\] of BaseType; 或 TypeName = Array \[IndexType1\] of ...Array \[IndexTypeN\] of BaseType; 其中 TypeName 代表所定义的数组类型名称。IndexType 代表数组各成员的标号类型。 BaseType 代表数组成员的类型。事实上,虽然 Delphi 允许定义多维数组,但实际应用中 一般只使用一维或二维数组。若定义一维的数组,声明语句则可简化成: Type typeName = Array \[IndexType\] of BaseType; 如定义一个一维数组类型: Type MyArray = Array \[5..9\] of integer; MyArray 类型的数组变量中含有 5 个成员,序号分别为 5、6、7、8、9。 定义一个多维数组类型: Type MultiArray = Array \[1..3\] of Array \[8..9\] of integer; 或 MultiArray = Array \[1..3,8..9\] of integer; MultiArray 类型的数组变量中含有 3 个成员,其中每成员又是一个含 2 个成员的数组。这 有点类似于教室中的座位,全班共分为若干组,每一组由有若干列组成。请读者思考下面 的数组 MultiArrayEx 的构成: Type SubArray = Array \[8..9\] of integer; MultiArrayEx = Array \[1..3\] of SubArray; 数组类型的变量可使用数组类型的名称来声明,也可直接使用数组类型的本体来声 明。如: Var A1:MyArray; A2:Array \[5..9\] of integer; - 42 - 数组变量赋值时只能按其成员逐个逐个赋值。Delphi 在处理数组变量时,使用数组类 型的名称来判断变量的类型。换而言之,若有数组 A1、A2 声明如下: Type A1:Array \[1..5\] of integer; A2:Array \[1..5\] of integer; 编译器会将 A1、A2 当成两个不同的类型,因为 A1 与 A2 所属的数组类型均无名称。只有两 个数组类型名称一致时编译器才会两个变量当成同一类型的变量。可将上面的声明语句改 成如下: Type A1,A2:Array \[1..5\] of integer; 2. 动态数组 与静态数组不同,动态数组在声明时只需声明成员的类型,并不需要指定成员的序 号。也就是说,动态数组并未在声明时指定成员的数量,其成员数量可以在运行期间动态 改变。 一个 N 维动态数组声明如下: Type TypeName = Array of Array of ... BaseType; 与静态数组声明不同,声明动态数组不需要指定成员序号。根据上式,声明一个一维动态 数组变量 dA1 的格式为: Type dMyArray = array of char; Var dA1:dMyArray; 当然也可以直接写成: Var dA1:array of char; 不同于静态数组变量,动态数组变量在赋值前必须设置大小。此过程可使用函数 SetLength 完成,其使用方式为: SetLength(A,n1,n2,...); A 表示任一数组变量,n1 表示 A 的第一维的长度,n2 表示 A 的第二维的长度...依此类 推。Delphi 并未要求一次性设置所有维的长度。故可先设置第一维的长度,必要时再去设 置第二维的长度。注意:对于多维数组,只有第 N 维的长度确定后才能指定第 N+1 维的长 度。 对于以下声明: Var dA:Array of Array of integer; 此语句中 dA 为二维数组变量,调用 SerLength: SetLength(dA1,4,2); 则 dA 共含 8 个成员:dA1\[0\]\[0\]、dA1\[0\]\[1\]、dA1\[1\]\[0\]、dA1\[1\]\[1\]、dA1\[2\]\[0\]、 dA1\[2\]\[1\]、dA1\[3\]\[0\]、dA1\[3\]\[1\]。上述表示法也可简写成 dA1\[0,0\]、dA1\[1,0\]... Delphi 提供 3 个标准函数:High()、Low()、Length()用于数组变量。其中 High()和 - 43 - Low()用于返回数组成员的序号的最大值与最小值,而 Length()用于返回数组的成员数量 即数组的长度。值得注意的是:动态数组只能设置数组的长度,不能设置成员的序号。而 Delphi 对于未指定成员序号的数组默认其序号从 0 开始。故而动态数组的第一个成员的标 号为 0,其长度比其最大序号要大 1。 1. 1. 1. 记录类型 Delphi 的新版本将 Record 类型作了扩展,但这种扩展涉及面向对象部分。为避免麻 烦,此节我们只讨论未经扩展之前的 Record 类型,新版的 Record 将置于后续章节。从这 个角度来说,本节的标题改为"传统记录类型"似乎更恰当一些。 闲话少说,我们开始。 1. 记录类型的定义 假设我们编写程序时需要使用某班同学的个人信息如身高、体重等,我们刻怎么表示 这些数据?最简单的想法就是针对每个学生的每条信息设置一个变量。如 StdA_Height、 StdB_Height 分别表示 A 同学、B 同学的身高等,但这样一来,若每个学生有 5 条信息,全 班共有 60 个学生,我们就得设置 300 个变量,不用说,这是很麻烦的事。 如果有这样一种变量,其中含有 5 个成员,每个成员均用于表示每个学生的一条信 息。这样一来我们只需要设置 60 个变量。显然这要轻松的多。 记录类型的变量符合上述要求。我们可以定义一个记录变量其中含有 height、high、 ID、name、StdClass 共 5 个成员分别表示学生的体重、身高、学号、姓名、班级。获取 A 同学的信息时,A.height 表示其身高,A.name 表示其姓名,如此类推。可以看出此方式比 起设置 300 个变量要好得多,最起码代码量要少得多。 一个记录类型声明如下: Type TRec = record Member1:type1; Member2:type2; ... memberN:typeN; End; 此语句声明了一种记录类型,其中含有 N 个成员,member1 的数据类型是 type1, member2 的数据类型是 type2,依次类推。 l 成员类型可以是任何类型。 l 声明中的每一行用分号隔开,但 typeN 后的分号可以不写。 根据上述声明方式,本节前面所述的用于统计学生信息的记录类型可声明为: Type Std = record Name:string; ID:string Height:integer; High:integer; - 44 - StdClass:integer End; 数据类型相同的成员可以写在同一行,上式可简写成: Type Std = record Name,ID:string; Height,High,StdClass:integer; End; 2. 记录变量定义及赋值 记录类型变量的定义有两种方式:一是使用记录类型的名称定义;二是使用一个记录 直接定义。不能将其中一种方式定义的变量直接赋给另一种方式定义的变量。无论记录变 量是用何种方式声明,但对于记录成员而言,只要赋值兼容即可相互赋值。 program RecordRExample; {$APPTYPE CONSOLE} type TStd = record //声明记录类型 TStd,用于描述每个学生的姓名和班级信息 Name:string; Grade:integer; end; var //声明 A、B、C 三个变量 A,C:TStd; B:record Name:string; Grade:integer; end; begin B.Name := 'BName'; //将 B 的姓名设为 BName A := TStd(B); //不能将 B 直接赋给 A,须经类型转化 C.Name := B.Name; //记录成员之间只要赋值兼容即可自由赋值 writeln(A.Name); //显示 A 的姓名 writeln(C.Name); readln; //使屏幕暂停滚动,按下任意键即可结束 end. 若要存取记录变量的成员,可使用 TypeName.Member 的形式来存取变量的成员。如 C.Name 表示 C 的姓名,则 C.Name := 'Bname'表示将 C 的姓名设为 Bname。 记录类型变量的赋值有两种方式: 一是利用另一个记录变量直接赋值。如上例中,对于 C 的赋值也可以直接定成: C:=A; 编译器会自动将 A 中的所有成员的值逐一赋给 C 中的相应成员。若记录变量中有以引用类 型的成员,则情况较为复杂,关于这点在后续章节中会有具体论述。 另一种方式是对记录变量的成员手动逐个赋值: C.Name := 'Bname'; - 45 - C.Grade := 2; 若记录变量中成员较多,可使用 with 语句简化赋值所需输入的代码: With C do begin Name := 'Bname'; Grade := 2; end; 3. 变体记录 假如你是公司职员,现在要编写一个程序统计公司员工的收入情况。你可以定义一个 记录类型来储存每个员工的薪水: TSalary = record name:string; MonthWage:Currency; end; 其中的成员用于记录员工的姓名及月薪。 现在有个问题,你可能是按月拿那一点可怜的月薪,但公司中某些领导干部却是拿年 薪的。这样一来,用这个记录类型怎么来记录这些人的薪水呢?当然我们不允许定义另外 一个记录。(其它的种种复杂情形也请读者吞到肚子里,这只是一个例子,经不起那么多 的可能性来折腾。) 你可以向其中再添加一个 YearWage 用于记录员工的年薪,但这样一来,每个记录中都 会有一个空闲的成员变量,是否有点浪费呢? 不知读者是否还记得前面介绍的共址变量,它可以很方便的解决这个问题。我们可以 将 TSalary 声明成如下形式: TSalary = record name:string; MonthWage:Currency; YearWage:Currency absolute MonthWage; end; 这样,我们不管使用 MonthWage 还是 YearWage 都不会造成浪费,因为二者共用同一块内 存。这就像两个轮流工作的人使用同一台计算机一样,虽然任何时候都有一个人在休息, 但计算机却不会空闲而造成浪费。 但是不幸的很,记录成员根本不能声明共址变量。简单来讲,上面的声明无法编译。 所以这种想法也只能束之高阁。 既然共址变量不行,我们就需要找一个与之类似的方法。变体记录就是这样的解决方 法。 变体记录的实质是在记录中声明若干个共用同一块内存的共址变量(只共用开始的一 部分内存,你不能指望一个 8 字节的字段与一个 2 字节的字段完全共用同一区域,但它们 可以共用开始的 2 个字节)。但不同于普通的共址变量,变体记录有着完全不同的声明方 式: type 记录名=record 字段 1:类型 1; - 46 - 字段 2:类型 2; ... 字段 n:类型 n; case \[tag:\] 有序类型 of 常量 1:(字段声明); 常量 2:(字段声明); ... 常量 n:(字段声明)\[;\] end; 其中的 case 之前的部分为普通记录声明。自 case 开始到记录声明的末尾均是变体记录的 专有部分。 首先,这部分声明必须置于所有普通成员之后。 其次,其中所有以方括号"\[\]"围起的内容均可省略。 第三,其中的所有常量必须是指定的有序类型。 第四,其中的每个常量可包括多个常量值,中间以逗号隔开。 第五:括号中的字段声明也可包括多个字段的声明,中间以分号隔开。其中的字段不 能被声明成长字符串、动态数组、变体类型、接口以及包含这些类型的其它结构类型如记 录、数组等。 第六:括号中的最后一个字段可不接分号。 第七:声明中的 tag 以及常量对于用户来说,根本没有任何用,我们只需保证其规范 性即可。 最后:虽然变体记录声明时使用了 case 关键词,但读者千万不要去想记录中的变体部分与 真正的 case 语句是否有什么关联。很确切的讲,至少在用户角度,没有任何关联,只是用 了同一个名字而已。使用变体记录时完全可将变体记录当成普通记录,编译器会处理所有 的细节。 下面声明一个变体记录的声明: type TRec = record s:string; case Integer of 1:(f1:integer; f2:String\[4\]); 2,6,8:(f3:string\[8\]) end; 笔者当初曾经很好奇,如果将 TRec 中的 f1、f2、f3 全部赋值,会有怎样的结果?请 读者也思考一下,对于如下示例,屏幕上会显示什么呢? var Rec:TRec; begin rec.s := '5'; rec.f1 := 4; rec.f2 := 'ABCD'; rec.f3 := 'Delphi32'; writeln(rec.f2); readln; end. 我们来分析一下,TRec 声明相当于声明了一个成员为 f1 和 f2 的记录类型的字段,然后再 让这个字段与 f3 共用一段内存,且二者占用的内存大小相等,所以它们完全地共用相同的 内存。明白这点,我们接着分析。 再替 f1 与 f2 赋值后,这段共用内存中 8 个字节的值已经确定为:0004ABCD,但 f3 被 赋值后,这 8 个字节的值又被恰好全部被 f3 的值所取代而变成:Delphi32,再然后,我们 在屏幕上显示 f2 的值时,它只能从这条字符串中选择属于自己的值来输出。由于 Integer 在内存中也占用 4 个字节,所以 f2 的值只能是共用内存的后 4 个字节,故而应该是显示字 符串 hi32。事实也正是如此。 读者可以再试一下将 TRec 中的 f1 与 f2 的声明交换一下顺序,或是将代码中 f2 与 f3 的赋值语句交换顺序,看看这两种情形下屏幕上会显示什么。 ### 3.3 字符串类型 字符串表示由字符所组成的序列,简单来说,若干个字符连在一起就可视为一个字符 串。Delphi 中常用的字符串类型主要有四种:Shortstring、Ansistring、Widestring、 Shortstring。三种字符串的简要信息如下: |----------------|----------|-------------|---------------------| | 类型 | 最大长度 | 所需内存 | 用途 | | ShortString | 255 byte | 2-256 bytes | 容纳由 AnsiChar 组成的字符串 | | AnsiString | 2 GB | 4 bytes-2GB | 容纳由 AnsiChar 组成的字符串 | | WideString | 2 GB | 4 bytes-2GB | 容纳由 WideChar 组成的字符串 | | UnicodeStri ng | 2 GB | 4 bytes-2GB | 容纳 Unicode 编码的字符串 | 上表中,UnicodeString 与 WideString 基本无区别。 Delphi 编程中通常将字符串变量声明为 String 类型,此类型与 UnicodeString 类型 完全等价,在较早的某些版本中 string 等价于 AnsiString 类型。 使用字符串时我们只需将其当成一个普通的简单变量,需要做的只是给其赋值并读取 值。所有的细节均由 Delphi 自动进行管理。下面是一个简单的示例,在其中我们声明了字 符串变量 STR,并给它赋值为'Delphi2010',然后在屏幕上显示。 var str:string; begin str := 'delphi2010'; writeln(str); end. Delphi2010 支持多种编码类型的字符串,使得字符串的应用非常方便,但这也直接导 致很多初学者甚至是有一定经验的 Delphi 用户迷惑不解。为了避免学习上的困难,此节我 们只讨论了常用字符串类型的基本知识以使读者能够毫无困难地学习接下来的内容。在本 书的附录中我们详细介绍了各种字符串,若读者有需要可参考。 ### 3.4 指针 指针类型是一类特殊的数据类型,此类型的变量专用于存储其它变量的地址(包括其它 指针的地址)。通常将指针类型的变量简称为指针。 举个例子:"夏威夷"相当于一个变量的名字,而"西经 157°、北纬 21°"则是这 个名字所代表的具体地址。指针就是一个专门用于表示某个变量在计算机中的具体地址的 数据类型。当一个指针变量含有某个变量的地址时,我们称这个指针变量指向了这个变 量。 看到这里读者不明白:既然可以使用变量的名称,为什么还要用指针呢?原因有两 个: l 某些情形下指针比变量名更方便,也更快捷 l 在调用其它语言编写的代码时可能需要指针 1. 指针变量的声明 Var 声明一个指针变量与声明一个其它变量并无明显区别,基本格式为: 变量名:指针类型 其中的指针类型可以使用符号"\^"加上一个合法的数据类型的名称表示。如声明一个字符 串指针变量: Var AnsiStr:\^AnsiString; 也可以先声明指针类型,再声明指针变量: Type PAnsiStr = \^AnsiString; Var AnsiStr:PAnsiStr; 这两种声明方法完全等同,只不过第一种更为简洁而已。 看到这里,读者可能发现了一个问题:指针还分为字符串类型、整型类型等不同类型 的指针?答案是 Yes。在未进行类型转换的情形下,Delphi 中的大部分指针类型必须和其 所指的变量的类型一致。如字符串变量只能用字符串类型的指针表示,而记录指针只能指 向记录类型的变量等。(只有极少数的指针才可指向多种类型的变量,这种指针称为通用 指针) 2. 指针的赋值 可以使用两种方式给一个指针变量赋值。其一是将一个指针的值直接赋给另一个指 针,赋值后二者将指向同一个变量;其二是接将某个变量的地址赋予一个指针变量。 Delphi 使用符号"@"加上变量名表示变量的地址。如@Var 表示变量 Var 的地址。符 号"@"称为取地址符,表示其专用于获得变量地址。类似地,符号"\^"也被称之解地址 符。一个指针变量名后跟"\^"时表示此指针所指变量的值。如 var P:\^integer; V:integer; begin V := 89; P := @v; end; P 指向变量 V,P\^表示 V 的变量值。 Delphi 中的取地址符"@"也可使用标准例程 Addr()代替。如@V 等效于 Addr(V)。唯 一的区别在于 Addr()不受编译指令$T 的影响。 注:当编译指令$T 处于$T-状态时,@所返回的指针为无类型指针,与其它所有类型的指针 保持兼容。当处于$T+状态时,@返回与变量同类型的指针。默认状态为$T+。 3. 无类型指针 在进入本节正题前,我们先来看一下指针本身的结构。 以 sizeof 可知任何指针变量在内存中均占用 4 个字节,这 4 个字节分成两部分:一部 分用于存储指针所指向的地址值,另一部分用于标识其指向的数据的具体类型,为方便起 见我们将此部分称为类型码区。当我们运行下面语句时; var i:\^integer; begin i\^ := 9; end; 计算机实质上进行了两步操作: 1. 判断 i 的类型码是否与所赋之值一致,若不一致则无法通过编译,若一致则执行下一步 2. 将所赋之值赋予指针所指变量 在读取 i 所指向的值时同样要先验证类型码的值,不同的数据类型按照不同的方式读 取,如一般情形下字符'A'不会被读取成数值 65。 现在我们进入正题。Delphi 中存在一种特殊的指针类型------Pointer,此类型指针结 构中的类型码不为任何值,故也称为无类型指针。与普通指针不一样,在不进行类型转化 时,Pointer 只支持两种操作: l 将另一个指针或地址值赋给 pointer 指针 l 将 pointer 指针赋给另一个指针。 由于类型码为空值,使用时不能直接通过 P\^的形式读写 P 所指向的变量的值,必须利 用类型转化将 Pointer 指针转化为其它类型的指针(此过程实质上是设置 P 的类型码为某 个特定值): var p:pointer; n:integer; begin p := @n; //令 P 指向变量 n n := 98; - 50 - writeln(pinteger(p)\^); //通过类型转化读取 n 的值 pinteger(p)\^ := 78; //设置 n 的值为 78 writeln(pinteger(p)\^); readln; end. 4. 动态指针 指针有两种:一是静态指针,此类指针在声明时即可确定其所指向的变量需要多少内 存\];二是动态指针,它不指向一个变量而指向某一块没有分配名称的内存(可以看成是无 名变量),这种类型的指针是典型的"用多少,拿多少"。 编译器不会在声明时给动态指针分配内存,因为它不知道这个动态指针到底需要多少 内存。编译器不知道,但我们肯定是知道的,所以动态指针需要用户在代码中手动给它分 配内存。所谓善始善终,所有的由我们手动分配出来的内存都必须再由我们手动来进行销 毁。 Delphi 提供了若干对用于分配及销毁动态指针的标准例程,其中最常用的有两组: procedure New(var X: Pointer); procedure Dispose(var P: Pointer); procedure GetMem(var P: Pointer; Size: Integer); procedure FreeMem(var P: Pointer); New 及 Dispose 用于替某个指针分配内存,所分内存大小由指针所指变量的类型决 定,如 integer 类型的指针将被分配 4 字节内存,Byte 类型的指针则被分配 1 个字节的内 存。当这些内存不再被需要时应当使用 Dispose 手动销毁。这种分配方式算得上是典型的 0 或 1 型:要么分配固定大小的内存,要么不分配。 相对的 GetMem 则可以分配任意大小 的内存,其中的 Size 参数指针指定了所分配的内存的大小,这块内存也必须使用 FreeMem 销毁。 下面的例子展示了两组标准例程的用法: var p1: \^integer; p2: PChar; begin New(p1); GetMem(p2, 40); //替 P2 分配 40 个字节的内存 FreeMem(p2); Dispose(p1); readln; End. - 51 - ### 3.5 变体类型 1. 变体类型 在介绍变体类型前,我们先来看一下系统如何识别不同数据类型的数据。以最简单的 整数为例,系统在存储整数时会根据不同的数据类型的名称而采用不同的内部结构存储来 整数值。不同的数据类型有不同的内部结构,当我们以整数数据类型如 integer 声明一个 变量时,系统在存储此变量值时就会以整数的内部结构来存储这个变量值。这个过程类似 于我们登录电脑,当我们输入某个用户名及密码时,系统会启用此帐户的各项设置。在给 变量赋值时若系统发现值不能被在存入此变量(如大小不符、结构不符等)时将会引发错 误,例如若我们将一个带小数点的值赋予整数变量时会无法通过。(有些语言在此种情况会 自动将值转换为对应的类型,但 Delphi 不支持这种转换) 变体类型是一种非常特殊的类型,说其特殊是因为它可以容纳多种不同类型的值。可 以说变体类型有点类似于能够根据所用钥匙的不同可以自动变换魔法锁,其可根据变量值 的类型而自动转换其内部存储结构以容纳所赋之值。将整数赋给它时,它的内部结构转换 为整型结构,而将实数赋给它时,其内部则转换实型结构。当然,与其它转换一样,变体 类型的这种变换也并非万能,其内部不能容纳以下类型的值: 记录、静态数组、集合、文件、类、类引用、指针 \[ 注:\]变体中的字符串另有规定,我们将在本节的末尾讨论。 计算机中,一个变体变量占据 16 个字节,这 16 个字节分为两个部分:变量值及变量 值的类型码。 变量值可以是一个普通的变量值,也可以是一个指向变量值的指针。类型码用于标识 当前的变量值的数据类型。Delphi 提供两种方式以获取变体变量中数据的实际类型: l 使用 TVarData 结构,此结构相当于 Record 版本的变体类型: var v:variant; begin v := 'Delphi'; if TVarData(v).VType = varUString then writeln('v 中的实际类型为 UnicodeString'); end. l 将标准函数 VarType 返回值与预定义常量 varTypeMask 进行 and 逻辑运算可返回变体 变量值的确切类型,VarType 在接受变体类型的参数 V 并返回 TVarData(V).VType: var v:variant; begin v := 2010; if VarType(V) = varDouble then writeln('v 中的实际类型为 Double 类型'); end. 若读者觉得以上两种方式不够方便,也可以直接获取相应类型所对应的常数值: writeln(VarType(V)) 或 writeln(TVarData(v).VType); 它们将返回一个整数形式的类型码。System 单元中声明了每种类型所对应的类型码,使用 时可自行查询。 定义后变体变量的初始值为预定义常量 Unassigned。常用的预定义常量 NULL 在变体 类型中表示未知的值或由于某种错误而丢失的的值。 默认情形下 NULL 值小于包括 UnAssigned 在内的任何值,但这并非绝对。Delphi 中提供了两个预定义变量: NullEqualityRule 与 NullMagnitudeRule。 NullEqualityRule 的值决定了当 Null 与其它值进行"="比较时的行为,而 NullMagnitudeRule 的值则决定了当 Null 与其它值进行大小比较时的行为。 编写代码时可以通过改变二者的值来控制 Null 值的行为。下面展示了二者可取之值 及各自所代表的意义: NullEqualityRule 的值及其含义 |-----------|-----------------------------------------| | 常量值 | 含义 | | ncrLoose | Null 与其它任何值均不相等。NullEqualityRule 默认为此值。 | | ncrStrict | Null 与所有值均不相等,即使另一个的值也为 Null | | ncrError | 参与比较的两个变量值之一或全部为 Null 时会引发运行期错误 | NullEqualityRule 的值及其含义 |-----------|-----------------------------------------------| | 常量值 | 含义 | | ncrLoose | Null 小于其它任何值(包括 Null)。NullMagnitudeRule 默认为此值 | | ncrStrict | 参与比较的两个变量值之一或全部为 Null 时,比较结果一定是 False | | ncrError | 参与大小比较的两个变量值之一或全部为 Null 时会引发运行期错误 | 2. 变体变量的赋值及使用 变体类型与普通类型间赋值兼容。我们可以直接将一个普通类型的值赋给变体变量, 反过来也可。唯一需要注意的是:所谓的普通类型一定是变量可以容纳的类型。若在表达 式滥用了变体类型及静态类型值,静态类型值将自动被转换成变体类型。如下面的示例: var V1, V2, V3, V4, V5: Variant; I: Integer; D: Double; S: string; begin V1 := 1; { integer 值 } V2 := 1234.5678; { real 值 } V3 := 'Hello world!'; { string 值 } V4 := '1000'; { string 值 } V5 := V1 + V2 + I; { real 值 2235.5678,I 转换成变体类型} I := V1; { I = 1 (integer 值) } D := V2; { D = 1234.5678 (real 值) } S := V3; { S = 'Hello world!' (string 值) } I := V4; { I = 1000 (integer 值) } S := V5; { S = '2235.5678' (string 值) } end; 若所赋之值超出了变体所能容纳的最大值,编译器自动反绕;而对于变体变量的不正 确操作如赋值、转型等均会引起编译错误。 操作符方面,除\^、is、in 之外的所有操作符均接受变体类型的运算数。除比较运算 总是返回一个逻辑型的值外,其它对变体变量执行的操作均将返回一个变体类型的值。这 条规则有个例外:当赋值符号的右边存在 Null 时左边的变量值一定是 Null。例如下面的 语句中 V 的值将为 Null: var v:variant; ... V := Null + 3; ... 这段代码有一点需要注意:Delphi 中并不存在 Null,代码中的 Null 实为定义于 Variants 单元中的标准函数,其声明为: function Null: Variant; 在程序的 uses 从名中添加 Variants 单元后,读者完全可以将 Null 当成一个预定义常量来 使用。 3. 变体类型转换 变体变量的类型转换可分为两种情况:一是将变体变量转换为其它类型的变量;二是 将变体变量的值转换为其它类型的值,如将变体变量值的类型由 varInteger 转换成 varSingle。 前者的操作形式与普通变量相同,均是以类型名进行强制转化,如 i := integer(v) 可将变体变量 v 转换成整数类型的变量并赋予整型变量 i。但转换的规则与普通变量间的 转换稍有差异,详见附录 B。将接口作为值赋给变体变量后,试图对这个变体变量进行任 何转换将导致编译器读取接口的默认属性值并将此值进行转换。若接口中未声明默认属性 则会引发一个异常。 对于后者,必须使用 Delphi 提供的预定义例程 VarAsType 与 VarCast 进行转换。函 数 VarAsType 声明于 Variants 单元,其声明式如下: function VarAsType(const V: Variant; AVarType: TVarType): Variant; 此例程将变体变量 V 的值转换为 AVarType 指定的类型。 VarCast 的声明为如下: procedure VarCast(var Dest: Variant; Source: Variant; VarType: Integer); 此例程也可将 Source 所代表的变体变量的值转换成所需类型并存储于 Dest 指定的变体变 量之中,varType 为预定义变量,与 VarAsType 中的 AvarType 类似。 下面是示例: uses SysUtils, Variants; var v1,v2:variant; begin v1 := 195; writeln(TVarData(v1).VType); //显示 17,表示 V1 的值为 Byte 类型 writeln(TVarData(v2).VType); //显示 0,表示 V2 的值为 Unassigned v2 := VarAsType(v1,varInteger); writeln(TVarData(v2).VType); //显示 3,表示 V2 的值为 Integer 类型 VarCast(v1,v2,varByte); writeln(TVarData(v1).VType); //显示 0,表示 V1 的值为 Byte 类型 readln; end. 4. 变体数组 Delphi 不允许将一个静态数组作为值赋给变体变量,但作为一个特例,我们可以将一 个静态的变体数组赋给变体类型的变量。Delphi 提供了两个标准例程 VarArrayCreate 与 VarArrayOf 用于创建变体数组。varArrayOf 声明式为: function VarArrayOf(const Values: array of Variant): Variant; 此例程将一个基类型为变体类型的普通数组转换为成员为 varVariant 类型的变体数组。如 下例所示: var v0:array\[0..3\] of variant; v1,v2:variant; begin v1 := VarArrayOf(v0); //v1 为变体数组变量,其值为一个含有 3 个成员的数组 v2 := VarArrayCreate(\[0,3\], varVariant); //v1 与 v2 完全相同 end. VarArrayCreate 的声明式为: function VarArrayCreate(const Bounds: array of Integer; AVarType: TVarType): Variant; 如前文介绍,AVarType 标识目标数组的成员类型,在 System 单元中可查看它的值 域。我们重点来看一下 Bounds 参数。 从声明中可知 Bounds 是一个以整型为基类型的动态数组。当我们所要创建的变体数组 是多维数组时,Bounds 参数中从第 1 个成员起每两个成员代表了多维数组的其中一维的最 小与最大成员序数值。由于 Bounds 的特殊用途,Delphi 规定了其成员数目必须是偶数且 不能超过 128,否则会产生运行错误。示例: v := VarArrayCreate(\[0,9,2,5\], varInteger); 此行代码创建的变体变量 V 的值是一个二维数组,其中第一维有 9 个成员,序数分别为: 0,1,2,3,4,5,6,7,8,9;第二维有 4 个成员,序数分别为:2,3,4,5。 我们声明一个普通的多维数组变量 V1: var v1 : array\[0..9\] of array\[2..5\] of Integer; 读者可能发现 v1 与变体数组变量 V 很相似。事实不然,二者有本质上的不同:变体数 组的成员并非一个单独的变量,无法获得其在内存中的单独地址,故而不能用指针指向变 体数组变量的某个成员;同样,作为例程参数时也不可使用 var 或 out 的传递方式。 当然,普通数组与变体数组变量也有相同之处,如二者均可被索引,不过变体数组变 量只能使用整数索引(普通数组的索引值可以是任一有序类型的值)。这也是 VarArrayCreate 的 Bounds 参数的基类型为 Integer 的原因。 在创建变体数组时,永远不要将 varString 传给 VarArrayCreate 以创建一个值为字符 串的变体数组,取而代之,我们可使用 varOleStr。 类似于普通动态数组,在程序运行期间可以使用 Delphi 提供的标准例程 VarArrayRedim(动态数组使用 SetLength)动态改变变体数组变量的成员数目,此类例程还 有: VarArrayDimCount VarArrayLowBound VarArrayHighBound VarArrayRef VarArrayLock VarArrayUnlock 关于这些例程的使用方法可参见 Delphi 的说明文档。 \[ 注:\]当某个变量的值为变体数组时,尽量不要将其赋值给另一个变量,因为这样会导致 其中所含的所有的数组值被复制,而这种复制通常都会导致效率低下。这意味着将这样的 变量作为例程参数时不适合选用默认的值传递及 const 传递方式,因为这两种传参方式传 递参数时也会发生赋值行为。 附:变体变量能够容纳的值的类型及对应的类型码: |-------------|-------|-------------------| | 类型标识符 | 类型码数值 | 对应类型 | | varEmpty | 0 | Unassigned | | varNull | 1 | Null | | varSmallint | 2 | SmallInt | | varInteger | 3 | Integer | | varSingle | 4 | Single | | varDouble | 5 | Double | | varCurrency | 6 | Currency | | varDate | 7 | Date | | varOleStr | 8 | BSTR 或 WideString | | varDispatch | 9 | IDispatch 接口 | | varError | 10 | Error | | varBoolean | 11 | Boolean | | varVariant | 12 | Variant | | varUnknown | 13 | IUnknown 接口 | | varShortInt | 16 | ShortInt | | varByte | 17 | Byte | |-------------|-----|--------------------------| | varWord | 18 | Word | | varLongWord | 19 | LongWord | | varInt64 | 20 | Int64 | | varUInt64 | 21 | UInt64 | | varString | 256 | ShortString 或 AnsiString | | varUString | 258 | UnicodeString | ### 3.6 运算符 ##### 3.6.1 有序类型运算符 Delphi 提供了 5 个所有的有序类型共有的运算符 ord、pred、succ、hign、low。从外 表看来,它们似乎有点像是标准函数。下面我们简要概括这些运算符各自的作用及用法。 1. ord:ord 可用于有序类型的表达式,主要用于变量,它返回指定值在值域中的序数 值。如 ord('A') = 65,因为大写字母 A 在字符集中的序数为 65。注意 ord 不接受 Int64 类型的参数。 2. pred:与 ord 一样,用于有序类型的表达式。它返回指定值的前一个值,如 pred('B') = 'A',而 pred(8) = 7。 3. succ:与 pred 相反,它返回指定值的下一个值,如 succ(8) = 9; 4. high:high 与下面的 low 可用于有序类型表达式及有序类型本身。high 返回变量能够 表示的的最大值或数据类型的上界,例如 high(byte) = 255。 5. low:与 hign 相反,它返回变量能够表示的的最小值或数据类型的下界。low 与 high 还可用于静态数组及短字符串。作用于短字符串时,它们分别用于得到数组的最大与 最小序数值。短字符串实质上也是数组,故而雷同。 1. 1. 1. 数学运算符 Delphi 中的数学运算符共有八个,其相关信息如下表所示: |-----|------|---------------|---------------|-------------------| | 运算符 | 运算功能 | 支持类型 | 返回值 | 示例 | | + | 相加 | integer, real | integer, real | X + Y | | - | 相减 | integer, real | integer, real | Result -1 | | \* | 相乘 | integer, real | integer, real | P \* InterestRate | | / | 相除 | integer, real | real | X / 2 | | div | 整除 | integer | integer | 34 div 3 = 10 | | mod | 求余 | integer | integer | 34 mod 3 = 4 | 1. 上表中的"支持类型"表示运算符能够进行运算的类型,如 div 的支持类型仅限整 数,表示 div 只能用于整数的相除,其返回值会自动舍弃小数部分,只保留整数。所 以 29 div 10=2。 2. x/y 的类型是 extended,无论 x 或 y 的类型是什么。对于其它的数学运算符,当运算 数中有一个是实型时,结果自动转化成 extended;但运算数中至少有一个是 Int64 时,结果转化成 Int64。当运算数是 Integer 的子界类型如 Byte、word 时,编译器将 其当成 Integer 类型。 3. Mod 表示求余,用于获得整数 X 除以整数 Y 后的余数,如 29 mod 10=9; 1. 1. 在 x mod y、x div y、x/y 中,y 不能等于 0。 除以上运算符外,Delphi 还提供了两一元运算符来进行正负运算: |-----|------|---------------|---------------|----| | 运算符 | 运算功能 | 支持类型 | 返回值 | 示例 | | + | 正运算 | integer, real | integer, real | +7 | | - | 负运算 | integer, real | integer, real | -X | 1. 1. 1. 逻辑运算符 逻辑运算符有四种:not、and、or、xor。 1. not 用于得到指定逻辑的相反值,它只有两种情形:not(True) = False;not(False) = True;例如当集合 myset 中存在元素 c 时,not (c in myset)的值为 False。 2. and 类似于数学上的"且"运算,对于 A and B,只有 A 和 B 都为 True 时才能得到 True,其它情形全部得到 False。 3. or 类似于数学上"或"运算,对于 A or B,只要 A 和 B 中有至少有一个为 True,结 果就为 True。当两者都为 False 时才会得到 False。 4. xor 表示"异或"运算,对于 A xor B,无论 A 与 B 的值是什么,当 A、B 值相同时,A xor B = False,当 A、B 的值不同时,A xor B=True。 逻辑运算符只能运算逻辑类型的值,也就是说 if not 0 then...无法编译,因为 0 不是逻 辑值,而 not 只能运算逻辑值。可以通过强制转型将 0 转化成逻辑值:if not boolean(0) then...。 在使用 or 进行运算时,若第一个运算数确定为 True,整个的表达一定返回 True,在 这种情形下,第二个运算将不会被计算。这种机制实际上只执行了判断语句中的一部分代 码,故而称为部分执行。 例如以下代码: function B1:Boolean; begin writeln('this is B1'); Result := True; end; function B2:Boolean; begin writeln('this is B2'); Result := False; end; begin if B1 or B2 then Writeln('ok'); //B2 不会被执行 readln; end. 同样,在使用 and 进行运算时,若第一个运算数的值为 False,第二个运算数也不会 被执行。读者可以将上例中的 if B1 or B2 then...改成 if B2 and B1 then...,然后运 行并查看结果。 1. 1. 1. 位运算符 每个字节都有 8 个位(bit)组成,位运算用于操作这些字节位。Delphi 提供了 6 个 位运算符,分别为:not、and、or、xor、shl、shr。 1. not 用于将字节中所有的位的值变成取相反值。一个位的值只可能有两种 0 或 1。所以 利用 not 运算时,它会将字节位中的 1 变成 0,0 变成 1。例如若某个值的二进制形式 为:0111 1001,那么对此值进制 not 运算后其二进制形式变成:1000 0110。 2. and 将两个运算符的相同字节位进行比较,若两个位都是 1 则返回 1,否则返回 0。如 对于 14 and 2=2,14 在内存中的二进制表示:0000 1110,2 在内存中的二进制表 示:0000 0010,两者进行运算的结果以二进制形式表现为:00000020,十进制表现为 2。 3. or 与 and 相反,当两个字节位的值都是 0 时返回 0,其它情况全返回 1。14 or 2= 14。 4. xor 在两个字节位相同时返回 0,不同时返回 1。所以 14 xor 2=12。 1. 1. shl 与 shr 较为麻烦: l 对于 X shl Y,编译器将 X 中的所有字节位整体左称 Y 的个字节位。如 byte 类型 的值 14 在内存中的表现形式为:0000 1110,14 shl 2 后其表现为:0011 1000。可以发现,右端空出的字节位以 0 补全,左端称出的字节位被丢弃。 l 在进行移动前编译器会将 Y 进行变化:假充 X 的类型在内存中占用 n 个字节位, 则变换后的 Y 值为原来的 Y 值与 n 求余后的值 n。例如,若 X 为 Integer 类型, 其在内存中占用 32 个字节,Y 的值为 40,则 X shl 40 会被变换成 X shl 8,因 为 40 mod 32=8。 l 当 X 为有符号整数时,其最高位为符号位,这个符号位不参与字节移动。 shr 在移动字节时会将字节整体右移,其它与 shl 完全相同。 所有的位运算符只能计算整数,其计算结果都是整数。在使用 shl 与 shr 例如 X shl Y 时,其中的 Y 也只能是整数。 以下是一些示例: var n:Integer; begin n := not 1.2; //错误 n := 4 shl 0.1; //错误 n := 100 shl 2; //正确 end; ##### 3.6.5字符串运算符 字符串运算符只有一个:"+"。它将两个字符串或字符连接为一个字符串。如: 'DEL'+'PHI' = 'DELPHI'。用于相加的两个字符串可以是任何的字符串类型,相加后得到 的字符串可以赋给任何字符串变量。在将得到的字符串赋给短字符串变量时若字符串的长 度过大,系统会自动截断,只保留前 255 个字节。 var s:shortstring; left:pchar; right:shortstring; begin left := 'delphi'; right := '2010'; s := left+right; writeln(s); readln; end. 这个例子中的 s、left、right 均可以是其它的任意类型,包括字符指针、长字符串、短字 符串、字符数组甚至可以是字符类型。 1. 1. 1. 集合运算符 Delphi 中的集合类型极其类似于数学的集合,其运算可分为三种类型: l 对两个集合间进行合并、相减、相交从而得到另一个新集合。 l 判断一个集合中是否含有某个值。 l 判断两个集合的关系,如判断一个集合是否属于另一个集合。 约定 S1、S2 表示两个同类型的集合,X 为任一与集合同类型的值。则各集合类型的运算法 则如下: |--------|-----------|----------------------------------------------------| | 运算类型 | 书写方 式 | 运算结果 | | 并集运算 | S1+S2 | S1 与 S2 所有不重复成员组成的新集合。如:\[1,2\]+\[2,3\]= \[1,2,3\] | | 交集运算 | S1\*S2 | S1 与 S2 中所有相同的成员组成的新集合。如:\[1,2\]\*\[2,3\]= \[2\] | | 差集运算 | S1-S2 | 去掉 S1 中所有与 S2 共有的成员后的新集合。如:\[1,2\]-\[2,3\] =\[1\] | | 判断是否相等 | S1=S2 | 判断 S1 与 S2 中的成员是否完全相同,如相同则返回 True,否则 返回 False | | 判断是否不等 | S1\<\>S2 | 若 S1 与 S2 中的成员不是完全相同则返回 True,否则返回 False | | 判断是否包含 | S1\> = S2 | 若 S1 中含有 S2 的全部成员,则返回 True,否则返回 False | |----------|---|----|----|---------------------------------------| | 判断是否被包 含 | S1\< = S2 ||| 若 S2 中含有 S1 的全部成员,则返回 True,否则返回 False | | 从属运算 | X | in | S1 | 若 X 是 S1 的成员,则返回 True,否则返回 False | | | | | | | 1. 1. 1. 指针运算符 本部分我们介绍除了@、\^之外的四个适用于指针的运算符。 "="运算符用于验算两个指针是否指向了同一个对象,若是则返回 True。 "\<\>"运算符用于验算两个指针是否指向了不同的对象,若是则返回 True。 "+"与"-"比较复杂一点,这两个运算符只能用于 PWideChar 及 PAnsiChar。 我们先定义两个字符指针:P1 和 P2,假设它所指向的对象在内存中占用 n 个字节。再 定义一个整数变量 I。 "+"仅用于一个字符指针与一个整数相加的情形,如 P1+I 或 I+P1,表示 P1 的起始 地址加上 n\*I 个字节后得到的新位置。 "-"仅用于两个字符指针相减或一个字符指针减去一个整数:P1-P2 或 P1-I。 P1-I 与 P1+I 类似,它从 P1 起始的地址减去 n\*I 个字节。 P1-P2 将 P1 的指针值减去 P2 的指针值,得到的值为两者的绝对数值除以 n 的商。例 如若 P1、P2 为 WideChar 类型的指针,两者的指针值分别为 100、120,则得到的值是- 10。 1. 1. 1. 关系运算符 关系运算符用于计算两个运算数间的关系,包括等于、不等于、大于、小于、小于或 等于、大于或等于,一共六种关系。下表归纳了这六种类型运算符的相关信息: |------|-------|-------------------|------|---|----|----|---|---| | 符号 | 关系 | 运算数类型 | 返回类型 | 示例 ||||| | = | 等于 | 简单类型、类、类引用、接口、字 | 逻辑值 | I | = | Max ||| | \<\> | 不等于 | 符串 | 逻辑值 | X \<\> Y ||||| | \< | 小于 | 简单类型、字符串、PChar 指针 | 逻辑值 | X | \< | Y ||| | \> | 大于 | 简单类型、字符串、PChar 指针 | 逻辑值 | Len || \> | 0 || | \< = | 小于或等于 | 简单类型、字符串、PChar 指针 | 逻辑值 | Cnt || \< | = | I | | \> = | 大于或等于 | 简单类型、字符串、PChar 指针 | 逻辑值 | I \> || = | 1 || ## 第四章 程序流程控制 Delphi 语言中能够控制流程的语句有三种:条件语句、选择语句、循环语句。下面 逐一介绍: ### 4.1 条件语句 所谓条件语句即根据某个条件是否满足而决定下一步程序的运行方式。 完整的条件语句的书写格式为: If \<条件表达式\> then \<语句 1\> Else \<语句 2\>; 其中的语句可以是简单语句,也可以是复合语句。 此语句首先判断条件表达式的值,若为逻辑 True 则运行语句 1,若为 False 则运行语 句 2。语句 1 和语句 2 可以是简单语句或复合语句,也可以是另一个条件语句。若某 If 语 句的语句 1 及语句 2 中至少有一个是另一个条件语句,则称原来的条件语句为复合条件语 句。 编译器将整个条件语句(if \<条件表达式\>Then\<语句 1\> else \<语句 2\>)看成是同一 语句,else 之前的语句 1 不可以分号作为结尾。 下面我们编写一个小程序 CalcFees。此程序的功能非常简单:根据用户输入的货物 重量计算所需的运费。运费的计算方法是:小于并等于 50kg 的部分运费为 0.25 元/kg;超 过 50kg 但小于并等于 100kg 的部分运费为 0.35 元/kg;超过 100kg 的部分运费为 0.45 元 /kg。 元。 uses 按这种方法,120kg 的货物所需运费为:50×0.25 + 50×0.35 + 20×0.45 = 39 SysUtils, Dialogs; //添加 Dialogs 单元 var Money, Weight: Double; str:string; begin Write('请输入货物重量:'); Read(Weight); if Weight \< = 50 then Money := Weight \* 0.25 else begin if Weight \< = 100 then Money := (Weight-50) \* 0.35 + 50\*0.25 else if Weight \> 100 then Money := (Weight-100)\*0.45 + 50\*0.35 + 50\*0.25; end; Showmessage('运费的金额为:'+FloatToStr(Money)+'元'); End. 上例中含有复合条件语句,读者可发现这样写法非常不直观。为了使代码更加有 条理,形如上例的复合条件语句通常写成如下形式: if Weight \< = 50 then Money := Weight \* 0.25 else if Weight \< = 100 then Money := (Weight - 50) \* 0.35 + 50 \* 0.25 else if Weight \> 100 then Money := (Weight - 100) \* 0.45 + 50 \* 0.35 + 50 \* 0.25; writeln('运费的金额为:' + FloatToStr(Money)); 这两种写法没有任何区别,但很明显第二种写法看起来更为直观。此种写法描述成一般 形式为: If 条件 1 then 语句 1 Else if 条件 2 then 语句 2 „ Else if 条件 N then 语句 N Else 其它语句; 系统执行时,将逐个判断每个条件,当遇到条件 X 的值为 True 时,将仅执行相应的 语句 X,然后跳出整个 If 语句而运行接下来的代码。 上式中除了 If 条件 1 then 语句 1 外,其余语句若无必要均可省略。 ### 4.2 选择语句 当我们需要从两种情形中选择其中一种时,条件语句非常适合。但若我们需要从多 种情形中选择一种时,需要使用 if...else if ...end 复合条件语句。事实上,此种情况 下我们有比复合条件语句更好的选择:选择语句: 选择语句的书写格式为: Case 选择表达式 of 常量 1:语句 1; ... 常量 N:语句 N; Else ... End; 其中的语句可以简单语句或复合语句。与条件语句一样,else 及其后的语句可以省略, 若未省略,则 else 语句必须放在最后。选择表达式的值必须为一个占用内存小于 32 字节 的有序类型的值。 执行选择语句时,系统先判断选择器表达式的值并与其后的 N 个常量值比较,若其 中的常量 X 值与选择器表达式的值相等则系统将仅执行语句 X(X 为 1 至 N 间的任一值)。若 所有的常量值与选择器表达式的值均不相乘,则系统将仅执行 else 后的语句。 与上节一样,我们依然通过一个例子来演示选择语句的用法。我们将要编写这样一 个程序:程序将根据读者输入的数目而在屏幕上显示相应数目的星号,但最多只显示 4 - 63 - 个。若读者输入的数目大于 4 将会出现一个提示。此程序代码如下: program EchoStar; {$APPTYPE CONSOLE} uses SysUtils; var N: integer; begin write('请输入一个 1 至 4 间的数字:'); read(n); case N of 1:writeln('\*'); 2:writeln('\*\*'); 3:writeln('\*\*\*'); 4:writeln('\*\*\*\*'); else writeln('所输的数字不在 1 至 4 之间'); end; readln; End. 选择语句中的选择器表达式可以是一个变量也可以是一个表达式(语法上也允许其为 一个常量,但这样显然没有意义),且其值的类型必须为有序类型。当选择器表达式的值与 其后的常量的类型不同时,编译器将自动进行类型转换。但这种转换有时可导致未知错 误,故而建议用户编程时手动显式进行类型转换。 ### 4.3 循环语句 Delphi 中支持三种类型的循环:While,Repeat,For。 1.While 循环 While 语句的语法格式为: While 条件表达式 do 循环语句; 循环语句可以是任何合法的语句,也可以是另一个循环语句。条件表达式返回一个逻辑 类型的值,当其为 True 时,系统执行其后的循环语句,每执行完一次循环语句时系统均会 判断条件表达式的值,若为 True 则断续执行,若为 False 则终止执行当前的 while 循环而 执行 while 循环后的语句。 以下程序将计算 1+2+3+„+N 的和,N 的值由用户指定(为降低复杂性,N 值不可大于 100): program SumInt; {$APPTYPE CONSOLE} uses - 64 - SysUtils; var n,i:1..100; Sum:integer; begin write('请输入一个 1 至 100 间的数字:'); read(n); sum := 0; //将 sum 的值初始化为 0 I := 1; while i \< = n do //当 i 的值小于 n 时执行循环体 begin sum := sum+i; i := i+1; //每执行完一次,i 的值增加 1 end; writeln(inttostr(sum)); //显示 sum 的值 readln; End. 注意例子中的 i := i+1,若没有这句代码,i 的值永远为 1,程序将会一直执行循环至 死机,即陷入死循环。 将 while 中的条件表达式省略时,也会致使此循环永不停止,这样的循环称之为空 循环。 1. 1. 1. Repeat 循环 Repeat 循环与 while 循环非常类似,唯一的区别在于:while 循环第一次时先判断 条件是否满足之后再执行循环休,而 repeat 循环第一次执行时先执行循环休再判断条件。 Repeat 语句的语法格式为: Repeat \[循环体

Until <条件表达式>

将前面的 SumInt 改写成 repeat 循环,其代码为: begin

write('请输入一个 1 至 100 间的数字:'); read(n);

sum := 1; //将 sum 的值初始化为 1 i := 1;

repeat

i := i+1;

sum := sum+i; //这两句语句的顺序不可调换 until i = n;

writeln(inttostr(sum)); //显示 sum 的值 read(i);

End.

  • 65 -

以下两个问题请读者自行思考:

l 为何 sum 的初始值由 0 变成了 1?

l Repeat 循环体中的两句语句的顺序能否调换?

      1. For 循环

当我们知道将要循环的次数时,可用 for 循环。其语法格式为; For 计数器 := 初值 to 终值 do

< 循环体 > 说明:

1.计数器必须为有序类型的变量,初值与终值必须为有序类型的常量或变量

2.计数器、初值、终值的数据类型必须相互兼容,最好全部相同

执行 For 语句时,系统先将初值赋予计数器(故而计数器不需要预先手动赋予初始 值),然后判断计数器与终值的关系,若计数器小于或等于终值,系统将执行一次循环,并

将计数器的值加 1。当计数的值大于终值时,系统将停止循环,此时计数器的值等于终值 加 1。当初值大于终值时不会发生循环。

For 语句还有另一种形式的格式: For 计数器 := 初值 downto 终值 do

<循环体>

此种形式与前一种形式相反,系统每次执行完循环后,会将计数器的值减 1,直到计数 器的小于终值为止。此种形式的 for 语句执行完毕后计数器值比终值小 1。显然,初值小 于终值时将不会发生循环。

注意:对于任何形式的 for 语句而言,当初值与终值相等时,系统只执行一次循 环。如下面的例子:

program ForSample;

{$APPTYPE CONSOLE}

uses

SysUtils; var

I,n:integer;

Begin

write('输入 n 的值:'); read(n);

for i := 1 to n do

writeln('Delphi2010'); //系统执行 n 次循环 writeln('i 的值为:'+inttostr(i)); //显示此时 i 的值

read(n); End.

上面的程序要求用户输入一个数字作为 n 的值,然后在屏幕上显示 n 行文字,当

for 执行完毕后 i 的值为 n+1。

当 n 的值为 1 时,只显示一行文字,且 for 语句执行完毕后 i 的值为 2。 注意,本例中 for 循环中的初值为 1,所以执行 n 次循环。当将初值改为其它值

  • 66 -

时,系统将 X 次循环,其中 X 的值为:

X = 终值 + 1 - 初值 若使用的是 for„downto„形式的循环,则 X 的值为:

X = 初值 + 1 - 终值 4.For...in 语句

Var

请读者思考一下,对于如下集合变量:

Myset:set of AnsiChar; Begin

Myset := ['A','B','C','D'];

End. 若现要求将其中的所有成员逐一在屏幕上显示出来,读者会怎么做? 可能读者会想到利用索引:

Var

Myset:set of AnsiChar; I:integer;

Begin

Myset := ['A','B','C','D'];

For i := 1 to 4 do Writeln(myset[i]);

End.

看起来很完美,但如果读者的记忆力还不错的话,就应该知道集合类型是不支持索引 的。所以以上代码无法通过编译。这咱时候我们可以使用 For...in 循环语句。

For...in 循环是 Delphi 新版本中新加入的一种循环语句,习惯上称为遍历语句,关 于其作用,笔者在此暂不介绍,若读者看完下面的例子后还不明白,那我说了也是白说。 遍历语句的语法格式为:

For V in set do <循环体> V 为任一合法变量。Set 为集合变量。V 与 set 的基本类型必须一致,至少相互兼容。 For...in 循环过程如下:先将集合中的第 1 个成员赋予变量 V 并执行循环体中的语 句,然后将第 2 个成员赋予 V 并执行循环体中的语句„依次类推,直到最后一个成员,此 过程称之为集合的遍历。注意:集合成员具有无序性,故系统执行时将根据赋值时各成员

出现的先后顺序来运行。如下面的例子: program ForSample;

{$APPTYPE CONSOLE}

uses

SysUtils;

Var

Myset:set of AnsiChar; ch:AnsiChar;

Begin

Myset := ['D','B','C','A'];

  • 67 -

for ch in myset do Write(ch);

readln; End.

执行以上代码,屏幕显示:DBCA。

除 Set 类型外,for...in 还能遍历以下类型:

l 数组。一维二维多维均可,动态静态都行。遍历时将按序号由小到大进行遍历。

l 字符串。按照前后顺序从第一个字符开始遍历。

l 类、接口、记录。对于这三种类型的变量,实现遍历功能的过程较为复杂,一般很少 使用,在此我们不再介绍。Delphi 预先提供了以下几个类用以支持对象的遍历: Classes.TList

Classes.TCollection Classes.TStrings Classes.TInterfaceList Classes.TComponent Menus.TMenuItem ActnList.TCustomActionList DB.TFields ComCtrls.TListItems ComCtrls.TTreeNodes ComCtrls.TToolBar

下面以 Classes.TStrings 给出一个相关的示例:

program Project1;

{$APPTYPE CONSOLE}

uses

SysUtils, Classes;

var

StrArray:TStringList; Item:String;

begin

StrArray := TStringList.Create; StrArray.Add('stringA'); StrArray.Add('stringB'); StrArray.Add('stringC');

for Item in StrArray do writeln(Item);

Readln; end.

运行这个程序,屏幕上显示以下 5 条字符串:

stringA stringB stringC

  • 68 -

可能读者会奇怪,这个例子中使用的类是 TStringList 而非 TStrings,为何?我们知 道,类具有功能,TStrings 中的成员能够被遍历,也就意味着其所有的子类也可被遍历, 而 TStringList 正是 TStrings 的子类。但在这个例子中我们不使用 TStrings 的原因并不 仅限于此,还有另一个重要原因是:TStrings 是个抽象类,其中仅仅只是声明了相关的方 法而并未具体这些功能,所以无法用于遍历。

以上例子同时也说明:对于任何一个类,只要这个类的某个祖先类支持遍历,这个类 就可以被遍历。

5.Continue 与 Break 语句

continue 与 break 虽非循环语句,但此二者只用于前面介绍的四种循环语句,故在此 一并介绍。continue 用于跳出当前正在执行的一次循环并重新开始新一次的循环。Break 用于停止循环而执行循环语句后面的代码。

下面我们将通过一个例子说明一下此二者在 for...in 循环中的使用,当用于其它循环 语句时作用与 for...in 中一样。

program BreakSample;

{$APPTYPE CONSOLE}

uses

SysUtils;

var

ch:char; str:string;

begin

str := 'ABCDEFGHIJKLMN';

for ch in str do begin

if ch = 'H' then break;

writeln(ch); end;

readln; End.

此程序遍历字符串 str 并显示其中的字符,但在循环体中设置了一条件:当遇到字符

串中的'H'时停止遍历,直接执行循环语句后面的代码即'readln;'。故而屏幕上只显示 'ABCDEFG'。

若将代码中的 break 改为 continue,循环体的条件则变为:当遇到'H'时不再断续执

行此次循环而会重新开始一次新的循环,所以屏幕上不会显示'H',只显示 'ABCDEFGIJKLMN'。

4.4 程序中止例程

利用程序中止例程可以使得一个正常的运行的程序强行中止。程序中止的结果有两 种,一是仅仅只退出当前正在运行的语句,但不一定会退出整个程序;二是直接退出整个

  • 69 -

程序。

Delphi 常用的程序中止方法有三种:使用 Exit 例程、使用 Halt 例程、调用全局程序 对象的 Terminate 方法。

  1. Exit 例程

Exit 声明于 Delphi 的标准 System 单元,其声明原型为:

procedure Exit; Exit 用于退出当前正在执行的程序块,当不会退出整个程序,除非将 Exit 用在了程序的 主程序块(即.dpr 文件中的程序块)中。下面的程序演示了 Exit 例程的用法:

procedure M1; begin

exit; writeln('M1');

end;

begin

writeln('Start Program'); M1;

writeln('Ending Program');

//Exit; readln;

end. 运行后的结果为:

Start Program Ending Program

若读者将例中的注释语句去掉注释符号后再运行程序时就会发现,程序执行后的窗口一闪

而过,根本不会停留在屏幕上。

在使用 try...finally...类型的异常处理语句时,在 try...finally 部分中执行的 exit 会被当成一个异常来处理。详见"异常处理"部分。

自 Delphi2009 开始,Exit 后可接一个参数以传递函数退出时所返回的结果。这种应 用必须满足两个条件:

  1. 后接参数的 Exit 只能用于函数中

  2. 参数的类型必须与函数的返回值的类型相同或兼容 下面的程序说明了这种用法:

function DoSomething(aInteger: integer): string; begin

if aInteger < 0 then Exit('Negative');

else

Result := 'Po