1.1数据类型
数据类型是每种编程语言不可缺少的一部分。ANSI-C 有 int,double 和 char 这几种。程序员们很少满足于现状,而编程语言通常会提供了从预定义的类型创建新数据类型的机制。一种简单的方法是构造集合如数组,结构体和联合体。被C. A. R. Hoare称为“我们永远无法收回的一步”的指针,允许我们表示和操作本质上极其复杂的数据。
到底什么是数据类型?我们可以从几个方面来看。数据类型就是值(value)的集合——char就有256种不同的值,int就更多了;两者都有均匀的间隔,或多或少有点像是数学里的自然数或整数。double类型的值就更多了,但是它们实际上却不像数学里的实数。
或者我们可以定义一种数据类型,它是一组值的集合以及作用于该集合之上的操作。一般来说,这些值是计算机能够表示的,而这些操作多少反映了可用的硬件指令。ANSI-C中的int在这方面上就做得不太好:值的集合在不同的机器上可能是不同的,并且如算数右移一样的操作也会不同。
更加复杂的例子没有更好的用处。一般我们可以用结构体来定义线性表的元素。
至于对线性表的操作我们指定像这样的函数头部:
但是,这种办法是很松散的。好的编程原则要求我们封装数据项表示,并且只声明可能的操作。
1.2抽象数据类型
如果我们不打算把具体表示暴露给用户,那么我们称这个数据类型是抽象的。理论上将这要求我们通过涉及到适当操作的数学公里来指定这个数据类型的属性。例如,只有当我们首先经常往一个队列里添加元素时我们才可以删除它,并且我们取回这个元素的书序与它们被加入的顺寻是相同的。
抽象数据类型为程序员提供了巨大的灵活性。既然数据表示不是类型定义的一部分,我们可以自由的选择更加简单或者最有效的方法。如果我们正确区分了必要的信息(数据类型的内部信息),那么数据类型的使用与我们选择实现的方法就完全独立了。
抽象数据类型满足了信息隐藏以及使用与实现分而治之的良好编程原则。信息(如数据项的表示)只要被提供给需要知道的它的人就可以了:是实现者而不是使用者。通过抽象数据类型我们可以清楚分开实现与使用:我们在把大系统分成小模块的道路上顺利前行。
1.3一个例子——Set
那么我们怎么实现抽象数据类型呢?作为一个例子我们考虑一个带有add、find和drop操作的元素集合。它们适用于一个集合、一个元素,并且返回添加的、查找到的或者从集合中删除的元素。find可以被用来实现contain,它告诉我们这个集合中是否已经包含了这个元素。
从这个角度,集合就是一种抽象数据类型。为了声明我们可以对一个集合做什么,我们创建了一个头文件Set.h
预处理语句保护下面的声明:无论我们包含多少次Set.h,C编译器只会看到它一次。这种保护头文件的技术是非常标准的,GNU C编译器可以识别它并且当这个保护符号(宏SET_H)已经被定义了的时候不会再访问Set.h。
Set.h是完整的,但是它有用么?我们几乎不能暴露或者显示更少的内容了:Set必须以某种方式来实现操作这个集合:add()获得一个元素并且将其添加进集合,返回该元素,无论这个元素是新加入的还是已经存在于集合中;find()在集合中查找一个元素,返回该元素或者空指针;drop()丁谓一个元素,从集合中将其删除,并且返回我们删除的元素;contains()把find()的结果转换成一个真值。
不幸的是,remove是一个用来删除文件的ANSI-C库函数。如果我们使用其命名一个set的函数,我们就不能再包含stdio.h
我们一直使用通用指针void*。一方面它使得了解一个集合到底有什么内容变得不可能,但另一方面它允许我们向add()和其他的函数传递任何东西。并使任何东西都会表现的像一个集合或者一个元素——为了信息隐藏我们牺牲了类型安全。然而,我们将在第八章中看到如何才能使这种方式变得实现。