购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4 程序语言的数据类型

数据类型是一组数据对象及创建和操纵它们的操作集合所组成的类。每种程序设计语言都有自己特定的数据类型系统,通常都会为程序员提供基本的数据类型。而现代语言大多都会提供用户自定义新的数据类型的机制,用于表示结构化或抽象的数据类型,例如,C++中为用户提供了自定义结构(Struct)、联合(Union)和类(Class)的机制。

一个数据类型中所有元素构成了这个数据类型的定义域,即该数据类型对象的取值范围。如果一个数据类型的定义域仅由常量值组成,则该数据类型就是标量类型,例如整型、实型、枚举类型都是标量类型。结构化数据类型和抽象数据类型的定义域中的元素都有自己的域,这些域又有自己的数据类型。

2.4.1 基本数据类型

基本数据对象只有单一的数据值,这种对象及其上的操作的集合组成的类称为基本数据类型。基本数据类型包含有内建数据类型(Built-In)、枚举类型和复合数据类型。

所谓内建数据类型是指程序设计语言提供的,无须用户另外定义的数据类型。各程序设计语言提供的内建数据类型都会有所不同,但通常都会提供整型、实型、字符型和布尔型4种内置数据类型。下面是一些程序语言提供的内置数据类型。

(1)Pascal:integer、real、boolean和char。

(2)C++:short、int、long、float、double、bool和char。

1.内置数据类型
1)数字数据类型

整型和实型都是数字数据类型,它们通常都是由硬件支持的。为整数类型定义的整数值的集合是一个有界的数学中研究的无限整数集的子集。类似地,为实数类型定义的实数值的集合是一个有界的数学中研究的无限实数集的子集。整型值和实型值在不同语言和不同计算机系统中的取值范围是不同的。

数字数据对象上的操作通常包括如下操作。

● 赋值操作。赋值操作的形式是number →number。即把一个数据对象的值赋给另一个数据对象。

● 算术操作。二元算术操作的形式是number x number →number。二元操作符可以是加、减、乘、除和求余(取模)等类似操作。要注意的是,两个不同类型的数字数据对象进行二元算术操作时,通常会将有效取值范围较小的数据对象转化为有效取值范围较大的数据类型,然后再进行算术操作。例如,在C++中,如果一个short类型数据对象与一个double类型数据对象进行加法运算,则会先把short类型数据对象转换为double类型,然后再进行运算,得到的结果为double类型的数据对象。一元算术操作的形式是x number→number。一元操作符通常有“-”和“+”等,还有是以函数库中的子程序形式提供的其他算术操作。

● 关系操作。关系操作的形式是number x number →boolean。关系操作符可以是大于、小于、等于、大于等于、小于等于和不等于等。关系操作比较输入操作数的大小,然后返回布尔型数据对象作为结果。需要注意的是,因为近似问题,很少有两个实型数的比较会是完全相等的。在进行实型数的比较时,通常是定义一个足够小的误差范围,例如ε=0.0001,当两个实型数的差的绝对值小于ε,则认为两个实型数相等。

● 位操作。在某些程序语言中,如C/C++,在整型数据对象上还定义了位操作,其形式为integerx integer → integer。C/C++中的位操作包括有与、或和移位等。

2)布尔类型

布尔型数据对象的定义域只有两个对象,true和false,表示逻辑值的真和假。在布尔型数据对象上的基本操作有赋值、相等判定、与、或和非,它们的形式分别是boolean x boolean →boolean(相等判定);boolean x boolean → boolean(与);boolean x boolean → boolean(或);boolean → boolean(非)。

3)字符类型

字符数据类型是以单个字符作为其值的数据对象。字符类型的定义域通常是由语言定义的和标准字符集有关的集合,该集合通常得到计算机硬件和操作系统的支持,如ASCII字符集。在字符集中的字符排序成为字符集的对照序列。对照序列为通过关系操作决定字符的顺序提供了依据。对字符类型的数据对象的操作有赋值、关系操作和检查字符是否属于某个字符集合(如可打印字符)的操作。

2.枚举类型

我们经常会让一个变量只取少量几个的值,如一周有7天,代表星期的变量就只会取7个值中的一个。虽然用整型数据对象可以表达该概念(例如用整数1~7表示星期),但是程序员没法保证不对这些变量进行无意义的操作(如Monday与Monday相乘是毫无意义的)。枚举类型使程序员可以更好地定义和操作这些变量。

枚举数据类型的取值范围是在列表中给出的,而在枚举数据对象上的运算为赋值和相等判定。

如一周的声明可以如下:enum DayOfWeek {Monday, Tuesday, Thursday, Friday, Saturday, Sunday};

3.复合数据类型
1)指针类型

指针是一个数据对象在内存中的地址。指针变量实际上是用来存放某个数据对象地址的变量。指针加上动态的数据对象的生成和对指针值的间接引用的机制,为程序员提供了处理可变大小的数据结构机制。

指针类型的单个数据对象可以有两种处理方式:

● 针对仅能引用一种类型的数据对象。指针数据对象的值被限制为指向同一类型的数据对象。在C/C++、Pascal和Ada中都是采用了这一方式。例如,在C++中,声明了指针变量:int* p;,则p仅能指向int类型的对象,而无法指向char等其他类型的对象。

● 针对可以引用任何类型的对象。这种方式允许指针数据对象在程序运行时指向多种类型的数据对象。SmallTalk就是这种方式的典型代表。

在某些语言中(如C或C++),指针是可以被程序操纵的数据对象,指针可以进行相减操作,指针也可以加上某个整数,以指向当前数据对象后面(或前面)的某个数据对象。而在另外一些语言中(如Java或C#),指针是由语言实现管理的隐藏数据结构的一部分,通常被称为引用(reference)。

在指针上的操作有生成操作和选区操作。

生成操作为固定大小的数据对象分配空间,同时生成一个指向新生成对象的指针,该指针存放在指针类型数据对象中。在C语言中,系统函数malloc提供了该功能,如需C语言中动态生成一个int类型的数据对象,可以使用以下语句:int* p = malloc(sizeof(int));

而C++、Java和C#等语言简化了malloc,提供了new函数来进行分配,上述语句在C++中可以改写为:int* p = new int;。

选取操作允许沿着指针来访问指定的数据。由于指针本身也是普通的数据对象,所以指针对象也可以使用普通的选取机制进行访问。在C/C++中,使用指针访问其指向的数据对象需要使用操作符*。例如,为上面生成的int型数据对象进行赋值,可以写为:*i = 0;。

2)字符串类型

字符串是由一个字符序列组成的数据对象。字符串数据类型至少有3种不同的处理:

● 固定长度说明。字符串具有固定长度,并且在程序中加以声明。

● 指定上界的可变长度。字符串对象有一个最大长度,这个最大值可以在程序中定义,而数据对象中的实际值的长度可能较短,甚至可以为0。

● 长度无限制。字符串数据对象可以是任意长度的字符串,字符串长度在运行过程中可以动态的变化。

C语言的情况比较特别,它把字符串看成是字符类型的线性数组,而没有专用的字符串声明。C语言的惯例是使用null字符('\0')作为字符串的结束符,表示字符串到此结束。

前两种字符串处理方法可以在编译时确定,而第3种处理方法则需要在运行时动态地分配字符串存储。

对于字符串数据对象的操作,一般有连接、关系操作、使用定位下标选取子串、格式化、模式匹配和动态字符串。

2.4.2 结构化数据类型

一个数据结构是一个包含其他数据对象作为其元素或成员的数据对象。比较重要的结构化数据类型有数组和记录。

1.数组

数组是编程语言中普遍的数据结构。一个数组是包含固定数目的相同数据类型成员的数据结构。我们用数组名、数组元素的类型、维数及下标来刻画一个数组的特征。对于数组,成员数量常由下标范围序列隐式给出,数组中的每个成员都是单一数据类型,而数组的下标集通常由连续整数组成。典型的一维数组声明是Pascal的声明:n : array[-10…10] of integer。该声明定义了一个有21个成员,各成员类型为integer的Pascal数组,各成员使用下标访问,下标的范围为-10,-9,…,0,…10。而在C语言中,数组的下标规定从0开始,如下面C中的数组声明:double a[10];。声明了10个成员的double类型数组,下标为0~9。

在给定下标值范围的语言中,下标范围不必从1开始,也不必是整数的范围,可能是枚举类型或枚举类型的子序列,例如:

对数组的操作通常是使用下标进行成员的访问,这个操作成为下标标定,下标标定返回左值或者该成员代表的数据对象的位置。如果给出了数组成员的地址,取得数组成员的值(右值)就变成了一个简单的操作。

C语言为用户提供了初始化数组的手段,对数组的初始化可以写为:

这里声明了一个有3个成员的float类型数组,其中fa[0]=1.0,fa[1]=2.1,fa[2]=3.1。另外,在C语言中,数组和指针的关系非常密切,常可以互换使用。如前面声明的数组fa,可以被看成一个float类型的常数指针(即其指针值不可变),使用指针操作可以达到与下标操作同样的效果,如表2-5中所示的左右两边的操作是等价的。

表2-5 指针操作和数组操作

2.记录

一个由固定数目的不同类型元素组成的数据结构称为记录。记录和数组都是有固定长度的线性数据结构的数据类型。记录与数组的不同之处在于:

(1)记录的元素是可以异构的,即可以由不同类型的数据元素组成;

(2)记录的元素使用标识符命名,而不是通过下标指定。

记录的属性有:元素的个数;每个元素的数据类型和每个元素的选择符。记录的元素通常称为字段,元素的名称称为字段名。请看下面的C语言结构的声明(结构是C语言中的一种记录):

该声明定义了一个date类型的记录,包含3个元素,id、score和class_id。

记录的一个基本操作是元素的选择,例如,student.id。记录元素的选择是需要明确给出所选择元素的名称。

C语言所提供的初始化手段也可以用于对结构的初始化,例如:

就声明了一个类型为student的结构对象stu,并且初始化id=3,score=80.5,class_id=205。

带有变体的记录类型使得该记录类型会发生变化。该记录类型的定义域就由所有这些变化可能产生的值集的集合组成。C语言提供的联合类型(union)正是该种数据类型,联合可以作为一个单独的类型进行处理,在不同的情况下它可以存储不同类型的数据。例如,以下C语言中的声明:

联合u既可以存储int类型数据,也可以存储float类型数据。我们可以把C中的联合看成一个特殊的结构,其中所有的成员相对于结构开始处的偏移量都为0,且结构的存储量大得足够存储最大的成员。在任一时刻,我们只能访问联合中的一个成员。

3.其他结构化数据类型

其他结构化数据类型包括有:

● 列表。列表是由一连串有序的数据构成的数据结构,通常是不定长和异构的。在JL、LISP和Prolog等语言中,列表是基本数据对象。列表的典型变形有堆栈(stack)、队列(queue)、树(tree)、有向图和属性列表。

● 集合。集合是一种包含无序的不同值的数据对象,集合中的值是不能重复的。

2.4.3 抽象的数据类型

抽象的数据类型的概念如下:

(1)数据对象的集合,一般使用一个或多个类型定义;

(2)在所定义的数据对象之上的抽象操作的集合;

(3)以上两个集合以如下规则封装起来,新的数据类型的用户不能直接操作这种类型的数据对象,即该类型的用户仅需要知道该类型的名字和可进行的操作的语义。

信息隐藏表示程序员定义的抽象设计中的原则,即每个这样的程序组件对于该组件的用户而言应该隐藏尽可能多的信息。当信息被封装在一个抽象中时,即意味着该抽象的用户无须知道所隐藏的信息即可使用该对象,且不允许他直接使用或操作隐藏的信息。一个成功的抽象就是让用户无须了解该抽象数据类型定义的数据对象的具体表示和相应操作所使用的算法。

在C++语言中提供了类(Class)这个新的定义抽象的数据类型的机制。C++语言中的类的成员包括有成员数据和成员函数,其中成员数据相当于抽象数据类型中的数据对象的集合,而成员函数相当于抽象操作的集合。C++语言通过访问存取控制关键字对成员的访问权限进行限制,它们分别是public、protected和private。为实现信息隐藏,通常会把成员数据的访问权限设置为protected或private,意味着只有该类对象和派生类对象可以访问或只有该类对象可以访问,也就是限制外部对象无法访问这些成员,而另一方面,把操作这些数据成员的函数的访问权限设置为public,允许外界通过这些函数对数据成员进行操作。下面是一个典型C++类的声明:

这个声明定义了一个堆栈类,使用数组作为数据的存储方式,h是栈顶的指针,通过pop()和push()进行出栈和压栈的操作。但是该类的用户完全无须了解类内部是如何实现的,只需要知道pop()和push()的语义就可以使用该类型的数据对象了。

2.4.4 类型和错误检查

前面讨论的类型检查都是围绕单个数据对象的类型及在该类型的操作上展开的。在程序设计中,最常用的数据对象是变量。程序语言需要确定变量是否应该具有固定的类型。类型是对变量的一个约束,如果对变量类型的约束发生在程序运行前,即变量在程序中一旦声明为一种类型,即不能改变其类型的,我们称类型的约束为静态的,也称早约束,如果对变量类型的约束发生在程序运行时,即变量在程序中的类型是可以改变的,我们称类型的约束为动态的,也称迟约束。Pascal和C语言都是静态类型约束语言,Lisp和Smalltalk则是动态类型约束语言。

进一步,我们可以把类型从单个的数据对象扩展至由数据对象和操作符组成的表达式。表达式的类型应该在编译时就已知且为固定的。

一个语言的类型系统就是一组规则,这些规则为语言中的每个表达式关联一个类型。如果一个表达式无法与某个类型相关联,那么类型系统就拒绝这个表达式。类型系统的规则也规定了每个运算符的正确使用。

1.类型检查的基本规则

类型系统中的规则基于函数的以下属性:从集合A到集合B的函数应用于集合A的元素,得到的结果是集合B的元素。

1)算术运算符

算术运算符是函数,对应每个运算符op都有一条规则规定如何由表达式E和F的类型确定表达式E op F的类型。

2)重载

运算符在不同的上下文中有不同的解释,可以接受不同类型的参数,并且可能根据参数的类型得到不同类型的结果,称为重载。常用的运算符,如+、–等都是重载的。例如运算符+,既可对整型进行加法运算,也可对实型进行加法运算。

3)隐式类型转换

当运算符需要的参数类型与实际参数类型不一致时,程序语言通常会在保证没有数据损失的前提下,自动地进行类型转换。例如,C语言表达式 2 * 3.14 * R中,R被声明为double类型,2是int类型,3.14是浮点类型,程序语言在不进行隐式类型转换的情况下无法确定表达式的类型。而在有隐式类型转换时,常数2和3.14都会被转换为double类型(因为double类型为此表达式中各数据对象的最高精度数据类型,这样的转换不会产生数据损失)。进行运算后,结果也为double类型。

4)多态

多态函数具有参数化的类型,参数化类型也称为类属性。多态类型允许只定义一次这类数据结构,以后可以应用到所需要的任何数据类型上。

在类似Pascal和C的命令式程序语言,仅有的多态函数是内部数据类型的运算符。C++利用模板机制来支持参数化类型。

2.类型等价

类型等价是在程序语言的类型检查中必然会遇到的问题。类型等价一般可以分为两种:

1)结构等价

结构等价的含义是:一个变量与自己的结构等价;如果两个类型结构是对结构等价的类型应用相同类型构造符而形成的,则该两个结构等价;在type n=T(C语言中使用tepedef n T)声明下,n与T等价。

2)按名等价

按名等价可分为:纯名字等价,类型名与其自身等价,但结构化类型不与任何结构化类型等价;传递名字等价,类型名与其自身等价,且可以声明为与其他类型名等价;类型表达式等价,类型名仅与其自身等价,两个类型表达式等价,如果它们是从等价表达式应用相同的构造方式而形成的。

3.静态和动态类型检查

类型检查是为了防止错误,保证程序中的运算应用都是正确的。如果某个函数或操作符接收到类型错误的参数,就会出现类型错误。如果程序执行中不会出现类型错误,则说它是类型安全的。

所谓静态类型检查,就是在编译时,编译器通过类型系统规则,根据程序的源文本,推断每次表达式f(a)求值时,参数的类型是否正确。所谓动态类型检查,是把额外的检查代码加插到程序中,在程序运行时进行类型检查。因为需要加插代码,因此动态检查的效率不如静态检查高,并且潜藏在程序中的类型错误需要到运行时才会被检查出来。由于两者的差异,一般程序语言只检查在源程序中可以静态检查的属性,而对需要在运行时才能进行检查的属性则很少进行检查。

程序语言的类型系统还有“强”、“弱”的属性之分。这里的强弱是一个相对概念,是指类型系统防止错误的有效性。一个强类型系统仅接受类型安全的表达式,不是强的系统就成为弱的。例如,相对来说,C++的类型系统就要比C强,也就是一部分C语言类型系统中可以接受的表达式,在C++类型系统中无法被接受的。 a0U60/HqcbHcvNyHnJJu3B68C9Hu3V4GXyk4mxrjG/UM/6GEdkGOvbeWI8ynmL+M

点击中间区域
呼出菜单
上一章
目录
下一章
×