不完全类型和抽象数据类型的定义
由于数组的下标是从0开始的,当index为0时,则getStackElemnt(stack, 0, &temp)返回栈顶的元素,getStackElemnt(stack, 1, &temp)返回接下来的那个元素,依此类推。
封装时,头文件中只放最小的接口函数声明,且内部函数都要加上static关键字。抽象栈的接口详见程序清单 2.33,接口揭示了栈的数据类型和用户在操作栈时需要的各种功能,这些功能实现了抽象栈类型的基本操作。
程序清单 2.33 抽象栈接口(stack.h)

这些函数共同创建了接口,每个函数都以stackADT作为它的第一个参数。当声明了函数接口后,即可实现相应的接口。
4. 实现接口
由于数组的长度在编译时就已经确定了,无法在运行时动态地调整。但有些应用在编译时并不知道应该分配多大的内存空间才能满足要求,因此可以根据需要使用动态内存"在运行时"为它分配内存空间。和任何接口一样,实现Stack.h接口需要编写一个模块Stack.c,它提供了抽象类型的输出函数和表示细节的代码,详见程序清单 2.34。
程序清单 2.34 抽象栈的实现(Stack.c)

表面上看起来getStackDepth()函数只有一行代码,也许有人会说,为何不直接使用"stack->top;"代替该函数呢?如果用户在程序中使用top,那么程序将依赖于stackADT表示的具体结构,而使用该函数的好处是为用户和实现之间提供了隔离层。由于维护代码是软件工程生命周期中的一个重要步骤,因此要尽量做好随时修改的准备。
当然,上述程序还是不能创建两种数据类型不同的栈,最常见的方法是使用void *作为数据类型,这样就可以压入和弹出任意类型的指针了。这里不再详细描述,将留给读者自己实现。但使用void *作为数据类型的最大缺点是不能进行错误检测,存放void *数据的栈允许各种类型的指针共存,因此无法检测由压入错误的指针类型而导致的错误。
5. 使用接口
实际上使用栈的人并不关心栈是如何实现的,即使要改变栈的内部实现方式,也不用对使用栈的程序做任何修改。将整数推入栈,然后再打印输出的范例程序详见程序清单 2.35。
程序清单 2.35 使用栈接口的范例程序

综上所述,Stack栈的接口分为两部分,其一是描述如何表示数据,其二是描述实现ADT操作的函数,因此必须先提供存储数据的方法。设计一个结构体,在".h"接口中定义栈的抽象数据类型stackADT,在".c"实现中定义栈的具体类型stackCDT。其次必须提供管理该数据的函数(方法),通过函数原型隐藏它们的底层实现。只要保留它们的接口不变,对于任何抽象都可以改变它的实现。实际上,当引入一个抽象数据类型stackADT时,就是在使用依赖倒置原则,将保存在结构体中栈的实现所需要的数据和处理数据的接口彻底分离,因为stackADT没有暴露它的细节,用户依赖于satcADT抽象,而不是细节。
显然,抽象数据类型可利用已经存在的原子数据类型构造新的结构,用已经实现的操作组合新的操作。对于ADT,用户程序除了通过接口中提到的那些操作之外,并不访问任何数据值。数据的表示和实现操作的函数都在接口的实现里面,与用户完全分离。抽象的接口隐臧不相关的细节,用户不能通过接口看到方法的实现,将注意力集中在本质特征上,将程序员从关心程序如何实现的细节上得到解放。对于任何抽象来说,只要保持接口不变,我们可以根据需要改变其实现方式。
- 单片机与程序设计(下)(08-13)
- 单片机与程序设计(上)(08-12)
- 周立功手把手教你学嵌入式编程:函数指针与指针函数的应用(07-29)
- 周立功教你学程序设计技术:做好软件模块的分层设计,回调函数要这样写(07-30)
- 周立功《程序设计与数据结构》:字符串函数(08-05)
- 周立功教你学程序设计结构体:内存对齐和基本数据类型(08-01)
