《C语言接口与实现:创建可重用软件的技术》一2.3 抽象数据类型

2.3 抽象数据类型

一个抽象数据类型是一个接口,它定义了一个数据类型和对该类型的值所进行的操作。一个数据类型是一个值的集合。在C语言中,内建的数据类型包括字符、整数、浮点数等。而结构本身也能定义新的类型,因而可用于建立更高级类型,如列表、树、查找表等。

高级类型是抽象的,因为其接口隐藏了相关的表示细节,并只规定了对该类型值的合法操作。理想情况下,这些操作不会暴露类型的表示细节,因为那样可能使客户程序隐含地依赖于具体的表示。抽象数据类型或ADT的标准范例是栈。其接口定义了栈类型及其5个操作:

〈initial version of stack.h〉≡
 #ifndef STACK_INCLUDED
 #define STACK_INCLUDED 

 typedef struct Stack_T *Stack_T; 

 extern Stack_T Stack_new (void);
 extern int   Stack_empty(Stack_T stk);
 extern void  Stack_push (Stack_T stk, void *x);
 extern void  *Stack_pop (Stack_T stk);
 extern void  Stack_free (Stack_T *stk); 

 #endif

上述的typedef定义了Stack_T类型,这是一个指针,指向一个同名结构。该定义是合法的,因为结构、联合和枚举的名称(标记)占用了一个命名空间,该命名空间不同于变量、函数和类型名所用的命名空间。这种惯用法的使用遍及本书各处。类型名Stack_T,是这个接口中我们关注的名称,只有对实现来说,结构名才比较重要。使用相同的名称,可以避免用太多罕见的名称污染代码。

宏STACK_INCLUDED也会污染命名空间,但_INCLUDED后缀有助于避免冲突。另一个常见的约定是为此类名称加一个下划线前缀,如_STACK或_STACK_INCLUDED。但标准C将下划线前缀保留给实现者和未来的扩展使用,因此避免使用下划线前缀看起来是谨慎的做法。

该接口透露了栈是通过指向结构的指针表示的,但并没有给出结构的任何信息。因而Stack_T是一个不透明指针类型,客户程序可以自由地操纵这种指针,但无法反引用不透明指针,即无法查看指针所指向结构的内部信息。只有接口的实现才有这种特权。

不透明指针隐藏了表示细节,有助于捕获错误。只有Stack_T类型值可以传递给上述的函数,试图传递另一种指针,如指向其他结构的指针,将产生编译错误。唯一的例外是参数中的一个void指针,该该参数可以传递任何类型的指针。

条件编译指令#ifdef和#endif以及定义STACK_INCLUDED的#define,使得stack.h可以被包含多次,在接口又导入了其他接口时可能出现这种情况。如果没有这种保护,第二次和后续的包含操作,将因为typedef中的Stack_T重定义而导致编译错误。

在少数可用的备选方案中,这种约定似乎是最温和的。禁止接口包含其他接口,可以完全避免重复包含,但这又强制接口用某种其他方法指定必须导入的其他接口,如注释,也强迫程序员来提供包含指令。将条件编译指令放在客户程序而不是接口中,可以避免编译时不必要地读取接口文件,但代价是需要在许多地方衍生出很多乱七八糟的条件编译指令,不像只放在接口中那样清洁。上文说明的约定,需要编译器来完成所谓的“脏活”。

按约定,定义ADT的接口X可以将ADT类型命名为X_T。本书中的接口在这个约定基础上更进一步,在接口内部使用宏将X_T缩写为T。使用该约定时,stack.h如下:

〈stack.h〉≡
 #ifndef STACK_INCLUDED
 #define STACK_INCLUDED 

 #define T Stack_T
 typedef struct T *T; 

 extern T   Stack_new (void);
 extern int  Stack_empty(T stk);
 extern void Stack_push (T stk, void *x);
 extern void *Stack_pop (T stk);
 extern void Stack_free (T *stk); 

 #undef T
 #endif

该接口在语义上与前一个是等效的。缩写只是语法糖(syntactic sugar),使得接口稍微容易阅读一些。T指的总是接口中的主要类型。但客户程序必须使用Stack_T,因为stack.h末尾的#undef指令删除了上述的缩写。

该接口提供了可用于任意指针的容量无限制的栈。Stack_new创建新的栈,它返回一个类型为T的值,可以作为参数传递给其他四个函数。Stack_push将一个指针推入栈顶,Stack_pop在栈顶删除一个指针并返回该指针,如果栈为空,Stack_empty返回1,否则返回0。Stack_free以一个指向T的指针为参数,释放该指针所指向的栈,并将类型为T的变量设置为NULL指针。这种设计有助于避免悬挂指针(dangling pointer),即指针指向已经被释放的内存。例如,如果names通过下述代码定义并初始化:

include "stack.h"
Stack_T names = Stack_new();

下述语句

Stack_free(&names);

将释放names指向的栈,并将names设置为NULL指针。

当ADT通过不透明指针表示时,导出的类型是一个指针类型,这也是Stack_T通过typedef定义为指向struct Stack_T的指针的原因。本书中大部分ADT都使用了类似的typedef。当ADT披露了其表示细节,并导出可接受并返回相应结构值的函数时,接口会将该结构类型定义为导出类型。第16章中的Text接口说明了这种约定,该接口将Text_T声明为struct Text_T的一个typedef。无论如何,接口中的主要类型总是缩写为T。

时间: 2024-08-02 19:33:28

《C语言接口与实现:创建可重用软件的技术》一2.3 抽象数据类型的相关文章

《C语言接口与实现:创建可重用软件的技术》一导读

前言 C语言接口与实现:创建可重用软件的技术 如今的程序员忙于应付大量关于API(Application Programming Interface)的信息.但是,大多数程序员都会在其所写的几乎每一个应用程序中使用API并实现API的库,只有少数程序员会创建或发布新的能广泛应用的API.事实上,程序员似乎更喜欢使用自己搞的东西,而不愿意查找能满足他们要求的程序库,这或许是因为写特定应用程序的代码要比设计可广泛使用的API容易. 不好意思,我也未能免俗:lcc(我和Chris Fraser为ANS

《C语言接口与实现:创建可重用软件的技术》一第1章 引言1.1 文学程序

第1章 引言 C语言接口与实现:创建可重用软件的技术 一个大程序由许多小的模块组成.这些模块提供了程序中使用的函数.过程和数据结构.理想情况下,这些模块中大部分都是现成的并且来自于库,只有那些特定于现有应用程序的模块需要从头开始编写.假定库代码已经全面测试过,而只有应用程序相关的代码会包含bug,那么调试就可以仅限于这部分代码. 遗憾的是,这种理论上的理想情况实际上很少出现.大多数程序都是从头开始编写,它们只对最低层次的功能使用库,如I/O和内存管理.即使对于此类底层组件,程序员也经常编写特定于

《C语言接口与实现:创建可重用软件的技术》一2.4 客户程序的职责

2.4 客户程序的职责 接口是其实现和其客户程序之间的一份契约.实现必须提供接口中规定的功能,而客户程序必须根据接口中描述的隐式和显式的规则来使用这些功能.程序设计语言提供了一些隐式规则,来支配接口中声明的类型.函数和变量的使用.例如,C语言的类型检查规则可以捕获接口函数的参数的类型和数目方面的错误. C语言的用法没有规定的或编译器无法检查的规则,必须在接口中详细说明.客户程序必须遵循这些规则,实现必须执行这些规则.接口通常会规定未检查的运行时错误(unchecked runtime error

《C语言接口与实现:创建可重用软件的技术》一2.6 扩展阅读

2.6 扩展阅读 自20世纪50年代以来,过程和函数库的重要性已经是公认的.[Parnas 1972]一文是一篇典型的论文,讨论了如何将程序划分为模块.该论文的历史已经将近40年,但当今的程序员仍然面临着该文所考虑的问题. C程序员每天都使用接口:C库是15个接口的集合.标准输入输出接口,即stdio.h,定义了一个ADT FILE,以及对FILE指针的操作.[Plauger,1992]一书详细描述了这15个接口及适当的实现,其叙述方式大体上类似于本书讨论一组接口和实现的方式. Modula-3

《C语言接口与实现:创建可重用软件的技术》一2.2 实现

2.2 实现 实现会导出接口.它定义了必要的变量和函数,以提供接口规定的功能.实现具体解释了接口的语义,并给出其表示细节和算法,但在理想情况下,客户程序从来都不需要看到这些细节.不同的客户程序可以共享实现的目标码,通常是从(动态)库加载实现的目标码. 一个接口可以有多个实现.只要实现遵循接口的规定,完全可以在不影响客户程序的情况下改变实现.例如,不同的实现可能会提供更好的性能.设计完善的接口会避免对特定机器的依赖,但也可能强制实现依赖于机器,因此对用到接口的每种机器,可能都需要一个不同的实现(也

《C语言接口与实现:创建可重用软件的技术》一2.5 效率

2.5 效率 本书中的接口的大多数实现所使用的算法和数据结构,其平均情况运行时间不会超过N(输入规模)的线性函数,大多数算法都能够处理大量的输入.无法处理大量输入的接口,或者性能可能成为重要影响因素的接口,可以规定性能标准(performance criteria).实现必须满足这些标准,客户程序可以预期性能能够达到标准的规定(但不会比标准好上多少). 本书中所有的接口都使用了简单但高效的算法.在N较大时,更复杂的算法和数据结构可能有更好的性能,但N通常比较小.大多数实现都只使用基本的数据结构,

《C语言接口与实现:创建可重用软件的技术》一2.7 习题

2.7 习题 2.1 原本可使用预处理器宏和条件编译指令如#if,来指定Arith_div和Arith_mod中如何处理除法的舍入操作.解释为什么对-13/5 == -2的显式测试是实现上述判断的更好的方法. 2.2 对于Arith_div和Arith_mod来说,仅当用于编译arith.c的编译器执行算术操作的方式与Arith_div和Arith_mod被调用时的目标机器相同时,这两个函数中所用的-13/5 == -2测试才是有效的.但这个条件可能会不成立,例如,如果arith.c由运行在机器

《C语言接口与实现:创建可重用软件的技术》一1.2 程序设计风格

1.2 程序设计风格 double说明了本书中程序所使用的风格惯例.程序能否更容易被阅读并理解,比使程序更容易被计算机编译更为重要.编译器并不在意变量的名称.代码的布局或程序的模块划分方式.但这种细节对程序员阅读以及理解程序的难易程度有很大影响. 本书代码遵循C程序的一些既定的风格惯例.它使用一致的惯例来命名变量.类型和例程,并在本书的排版约定下,采用一致的缩进风格.风格惯例并非是一种必须遵循的刚性规则,它们表示的是程序设计的一种哲学方法,力求最大限度地增加程序的可读性和可理解性.因而,凡是改变

《C语言接口与实现:创建可重用软件的技术》一1.3 效率

1.3 效率 程序员似乎被效率问题困扰着.他们可能花费数小时来微调代码,使之运行得更快.遗憾的是,大部分这种工作都是无用功.当猜测程序的运行时间花费在何处时,程序员的直觉非常糟糕. 微调程序是为了使之更快,但通常总是会使之更大.更难理解.更可能包含错误.除非对执行时间的测量表明程序太慢,否则这样的微调没有意义.程序只需要足够快即可,不一定要尽可能快. 微调通常在"真空"中完成.如果一个程序太慢,找到其瓶颈的唯一途径就是测量它.程序的瓶颈很少出现在预期位置或者是因你所怀疑的原因导致,而且