2.4 PascaI语言数据类型结构
Pascal语言数据类型结构是大家所熟知的。我们将以Pascal报告原始定义为基础进行讨论,以使读者对程序设计语言的数据类型有更进一步的认识。
2.4.1 非结构类型
Pascal的数据类型最终建立在非结构的原始成分上。非结构类型有内部的,也有用户定义的。内部非结构类型是integer,real,boolean和char。
类型integer是有序整数集的一个子集,这个子集的大小由具体实现定义。实现定义的最大整数用标准标识符maxint表示。类型real的值是实现定义的浮点数子集的一个元素。实数的最大值和精度依赖于实现和机器的基本浮点运算所受到的限制。类型char的值是一个有限字符集的元素,这个字符集及其字符间的顺序关系都是由实现来定义的。类型boolean只有true和false两个值,它们可以比较,false看成比true小,并由布尔运算符and,or和not对它们进行操作。
整型、布尔型和字符型统称有序类型(Ordinal Type)。这些类型的每个元素都有唯一的前驱(Predecessor)和后继(Succession),分别由内部函数pred和succ计算有序的前驱和后继值。布尔值true定义成false的后继。函数succ和pred接受上述几种有序类型的参数,因而它们是超载操作符的又一例子。
Pascal有两种定义新的有序类型的方法。
第一种方法是枚举所有可能的值,例如
type day =(sunday,monday,tuseday,wednesday,thursday,friday,saturday);
是用枚举方法说明的一个新的有序类型day,这是用户自定义的非结构类型,其效果如下:
① 引入一个新数据类型day;
② 定义day由sunday等7个元素组成;
③ 定义一个顺序sunday<monday<…<saturday;
④ 隐含对这个新类型变量可以进行的赋值和比较等操作。
因此,在变量说明
var today,tomorrow,yesterday,my_birthday:day;
之后,下列语句对变量所进行的操作是合法的:
today:=thursday; tomorrow:=succ(today); yesterday:=pred(today); my_birthday:=tomorrow;
遗憾的是,枚举类型的值不能直接读/写,虽然day包含了一个星期的名字,还是必须以某种编码方式(例如用整数)来读day的值,然后显式将其转换为类型day的值。
第二种方法是规定一个有序类型的子界(Subrange)。例如,我们已经定义了day,现在可以定义一个类型work_day作为由monday到friday的子界。下列程序段说明了这种情况:
type work_day = monday..friday; age = 0..120; var my_age :age; class_day :work_day;
若新定义的类型没有给出显式名字,也是可以使用的。例如
var course_taught :(comp_sci_15,comp_sci_240);
规定course_taught为匿名类型的变量,它是一个枚举类型的变量,该枚举类型仅具有值comp_sci_15,comp_sci_240。语句
course_taught:=comp_sci_240; my_age:=33; if course_taught = comp_sci_15 then class_day:=tuesday else class_day:=wednesday
是合法的句子。
子界类型的变量与它的基类型的变量具有相同的性质,但子界类型的值的集合仅是基类型值的集合的子集,这个性质通常在运行时进行检查。例如语句
class_day:=succ(class_day)
在运行时,若class_day已经为friday,那么它的后继应该是saturday,而saturday不属于子界类型work_day,它将导致一个运行(时)错误。
2.4.2 聚合构造
1.数组构造
array构造符允许程序员定义有限映像。数组构造的一般形式为
array[tl]of t2
其中,t1是下标(或定义域)的类型,要求是有序类型;t2是元素(值域)的类型,它是数组元素的类型。例如说明
type flavor=(chocolate,mint,peach,strawberry,vanilla,bluecheese,cataup,garlic, onion); icecream-flavor=chocolate..vanilla; icecream-order=array[icecream-flavor]of boolean; var my-order,your-order:icecream-order; choice:icecream-flavor;
之后,可使用下列语句:
for choice:=chocolate to vanilla do my-order[choice]:=false;
数组访问
my-order[succ(choice)]
是合法的,但是需要在运行时检查下标是否越界。实际上,若choice=vanilla,那么succ(choice)产生bluecheese,即产生一个不属于类型icecream-flavor的值,出现越界错误。
Pascal把下标类型不同的数组看成不同的类型,例如说明
type a1=array[1..50]of integer; a2=array[1..70]of integer;
定义了a1和a2两个不同的类型。在Pascal原始报告中,这样定义的数组类型会产生严重问题,它使得以形参定义的数组与替换的实参数组可能类型不同。换句话说,过程需要一个数组形参时,这个数组的类型是在过程定义时定义的,我们无法写一个过程,既能传递类型为a1的实参,也能传递类型为a2的实参。
在Pascal ISO标准中,为了解决上述问题引入了符合数组(Conformant Array)的概念,它能完成将形参数组大小匹配到实参数组大小的功能,要求实参和形参具有相同个数的下标,以及相同的成分类型。例如过程
procedure sort(var a:array[low..high:integer]of ctype); var i:integer; more:boolean; begin{sort} more:=true while more do begin more:=false; for i:=low to high-1 do begin if a[i]>a[i+1]then begin move-right(i); more:=true end; end; end; end{sort};
使用了符合数组,当过程sort被调用时,实参是一个一维数组;形参的low和high需要绑定于实参的下界和上界。
然而,符合数组只解决了部分问题,例如,它仍不能在sort中说明一个大小为how..high的局部动态数组(Local Dynamic Array),因为绑定的子界必须在编译时是常数,所以不可能说明类型为low..high的变量i。
Pascal可以定义多维数组,例如
type row=array[-5..10]of integer; var my-matrix:array[3..30]of row;
其中,变量my-matrix是一个元素为数组的一维数组,即二维数组,通常缩写成
var my-matrix:array[3..30,-5..10]of integer;
2.记录构造
构造符record可以用来定义笛卡儿积。记录结构的一般形式为
record field-1:type-1; field-2:type-2; … field-n:type-n; end
其中,field-i(1≤i≤n)是域标识符(Field Identifier),type-i(1≤i≤n)是相应域的类型。记录可以整体访问,也可用圆点“.”作为选择符访问单个的域。例如说明
type reg-polygon=record no-of-edges:integer; edge-size:real end; var t,q,p:reg-polygon;
定义了一个记录类型reg-polygon及其变量t,q和p。这样,下列语句
t.no-of-edges:=3; t.edge-size:=7.53; q.no-of-edges:=t.no-of-edges+1; q.edge-size:=2*t.edge-size; p:=q;
都是合法的。其中,t定义成边长为7.53的正三角形,而q和p定义成边长为15.06的正方形。
Pascal记录类型允许有可变部分,支持判定或。例如说明
type dept=(housewares,sports,drugs,food,liquor); month=1..12; item=record price:real; case available:boolean of true:(amount:integer;where:dept); false:(month-expected:month) end;
定义了一个可变记录类型item。其中,标识符域available是记录结构的判定成分,若available的值为true,那么可以用amount和where来刻画,否则以month-expected来刻画。
Pascal允许程序员访问记录结构的所有域,包括标识符域。因此,若i1和i2被说明,那么,可对它们进行如下操作:
var i1,i2:item; …… i1.price:=5.24; i1.available:=true; i1.amount:=29; i1.where:=liquor; i2.price:=324.99; i2.available:=false; i2.month-expected:=8;
其结果如图2-1所示。
图2-1 Pascal变体记录结构示例
Pascal把上述记录结构称为变体记录,它的类型检查只有在运行时进行。例如,i是类型item的变量,那么
i.amount
仅当标识符域的当前变体为true值时,才是一个正确的选择。
若改变一个变体记录的标识符域,就似乎(在概念上)建立了一个新记录,但这个新记录的所有域都未被初始化。例如,在上述语句之后加上语句
i1.available:=false;
就建立了一个i1的新变体。这时i1尚未初始化,所以还不能取域i1.month-expected的值。然而,大多数Pascal允许实现这样的访问,原因之一在于若禁止这种访问,运行时要付出很高的代价来进行检查,有时甚至难以实现。另一个原因是使用变体记录的方法允许Pascal逃避严格的类型检查。实现上,变体记录的传统实现都是在同一存储区上重叠放置所有变体(参见图2-13),因此,变体记录允许程序员根据每个变体的类型,以不同的观点来解释存储在该区域中的位串。在上例中,若变量i1的域amount和where已经赋值,再置available为false来改变变体,然后再按month-expected的值来解释先前存储在amount的值。
图2-13 判定或类型变量的表示
通过上述讨论可知,使用变体记录尽管是实际的,但也是不安全的。因为在这种情况下,同一存储区实际上可能对应两个不同的变体,也就具有不同的名字和类型。尽管它对某些问题的模式化带来了方便(例如,一个模拟输入设备的过程可以把一个变量看成字符序列,而另一个模拟专用处理器的过程可以把同一变量看成待读的整数),但一般来说这是危险的(例如,若两个名字都指同一数据对象,且都是可见的,那么以一个名字对这个数据对象进行修改,显然会影响到另一个名字),这样的程序既难读也难写。若以不同类型来看待某个位串,程序员就必须知道编译器如何表示不同的类型,使得程序编写完全依赖于实现。例如,要具体了解实现中几个字符与一个整数占相同的存储区(一个字长),在4个字符占一个字的实现上通过的程序,不能搬到一个字符占一个字的实现上,这样会出错。
由于Pascal的变体记录标识符域的标识符是可省略的,也使得Pascal变体记录不安全。例如,上述类型item可以说明成
record price:real; case boolean of true:(amount:integer;where:dept); false:(month-expected:month) end;
在这种情况下,由于记录中没有一个域能表示当前可用的变体,所以变体记录自身就是不完全的,无论是amount还是month-expected都可能在它们尚未出现之前就被使用,甚至在运行时也无法查出这种错误。尽管可以让编译程序插入一个标识符域来解决这个问题,但这没有实际意义,因为程序员为了节省存储空间,经过慎重考虑才确定默认标识符域。
ALGOL 68的“联合模式”与Pascal的变体记录一样,都支持判定或,但ALGOL 68的联合模式是安全的。Pascal变体记录外部形式简单,非常通用,然而它所存在的问题不容忽视。
3.集合结构
set构造符是幂集构造受限制的形式,其基类型只能是有序类型,因此它不可能定义实数、表和集合等的集合。实际上,每个实现都必须对基类型强加一个限制,大多数实现都不支持整数的集合。
下列说明定义一个枚举类型和两个该类型的集合变量:
type vegetable=(bean,cabbage,carrot,celery,lettuce,onion,mushroom,zucchini); var my-salad,leftover:set of vegetable;
以上定义了两个集合类型变量,其基类型为vegetable,因而下列语句是合法的:
leftover:=[bean..lettuce]; my-salad:=[carrot..onion]; 赋一个有4个成员的集合值 if not beam in leftover then my-salad:=my-salad+leftover; +表示集合的联合 4.文件构造
Pascal文件是任意类型的若干元素的序列。下列说明定义文件变量t1和t2:
type pattern=record…end; type file=file of pattern; var t1,t2:type file;
每个文件都自动地引入一个缓冲区变量(Buffer Variable)与之对应,它包含文件的下一个元素。程序能读/写缓冲区,get操作读下一个元素到缓冲区,put操作把缓冲区的内容附加到文件末端。Pascal文件仅能顺序处理,即由操作put和get隐含了更新文件的当前位置。
2.4.3 指针
指针是Pascal的第三类数据类型,是非结构的,可用来构造结构(递归)数据。
指针可引用匿名数据对象,这类数据对象是由建立语句new显式分配在堆(H eap)上的。下列说明
type tree-ref=↑binary-tree-node; binary-tree-node=record info:char; left,right:tree-ref end; var my-tree:tree-ref;
使用了Pascal指针定义一个二叉树数据对象,其中my-tree定义成指向字符二叉树根结点的指针(↑)。赋值语句
my-tree:=nil;
构成空二叉树。任何指针都可以保存nil值,它指向的对象类型是独特的,即不指向任何元素,这样的指针称为空指针(Null Pointer)。若P不是空指针,那么P指向的对象用P↑表示。语句
new(my-tree); my-tree↑.info:=symbol; my-tree↑.left:=nil; my-tree↑.right:=nil;
建立了只有一个结点的二叉树。其中第一个语句分配一个类型为binary-tree-node的记录,后三个语句使这个记录初始化。域info被赋予变量symbol的字符值,后两个指针指向nil。图2-2说明了这个记录结构,其中假定类型为Char的变量symbol的字符值为a。语句
图2-2 具有一个结点的二叉树
new(node-ref); node-ref↑.info:='b'; node-ref↑.left:=nil; node-ref↑.right:=nil; my-tree↑.left:=node-ref;
实现在my-tree上加一个左(子)树,图2-3说明了结果的结构。
图2-3 在图2-2所示二叉树上加左子树的二叉树
对指针的操作只能是赋值和比较(相等或不等),这些操作只有在两个指针指向对象的类型是一致的情况下才是合法的。Pascal指针只能指向匿名数据对象,实际上不可能指向栈上分配给有关变量的存储单元。
我们已经对Pascal的数据类型进行了简要的分析和讨论,图2-4给出了该类型结构的概貌。
图2-4 Pascal语言的数据类型结构