C和指针 第17章 经典数据类型 17.5 实现的改进

17.5 实现的改进
    本章的实现方式说明了不同的ADT是如何工作的。但是,当用于现实的程序时,它们在好几个方面是不够充分的。本节的目的是找出这些问题并给出解决建议。我们使用数组形式的堆栈作为例子,但这里所讨论的技巧适用于其他所有ADT。
    17.5.1 拥有超过一个的堆栈
    到目前为止的实现中,最主要的一个问题是它们把用于保存结构的内存和那些用于操纵它们的函数都封装在一起了。这样一来,一个程序便不能拥有超过一个的堆栈!
    这个限制很容易解决,只要从堆栈的实现模块中去除数组和top_element的声明,并把它们放入用户代码即可。然后,它们通过参数被堆栈函数访问,这些函数便不再固定于某个数组。用户可以创建任意数量的数组,并通过调用堆栈函数把它们作为堆栈使用。
    警告:
    这个方法的危险之处在于它失去了封闭性。如果用户拥有数据,便可以直接访问它。非法的访问,例如在一个错误的位置向数组增加一个新值,但并不调整top_element,都有可能导致数据丢失,或者生成非法数据,或者导致堆栈函数运行失败。
    一个相关的问题是当每个堆栈函数被调用时,用户应该确保它传递正确的堆栈和top_element参数。如果这些参数发生混淆,其结果就是垃圾。可以通过把堆栈数组和它的top_element值捆绑在一个结构里来减少这种情况发生的可能性。当堆栈数据包含数据时,就不存在出现上述两种问题的危险性。本章的练习部分描述了一个修改方案,允许堆栈管理超过一个的堆栈。
    17.5.2 拥有超过一种的类型
    即使前面的问题得以解决,存储于堆栈的值的类型在编译时也已固定,它就是stack.h头文件中所定义的类型。如果需要一个整数堆栈和一个浮点数堆栈,就没那么幸运了。
    解决这个问题最简单的方法是另外编写一份堆栈函数的副本,用于处理不同的数据类型。这种方法可以达到目的,但它涉及大量重复代码,这就使得程序的维护工作变得更为困难。
    一种更为优雅的方法是把整个堆栈模块实现为一个#define宏,把目标类型作为参数。这个定义然后便可以用于创建每种目标类型的堆栈函数。但是,为了使这种解决方案得以运行,我们必须找到一种方法为不同类型的堆栈函数产生独一无二的函数名,这样它们相互之间就不会冲突。同时,必须小心在意,对于每种类型只能创建一组函数,而不管实际需要多少个这种类型的堆栈。这种方法的一个例子在17.5.4节描述。
    第三种方法是使堆栈与类型无关,方法是让它存储void *类型的值。将整数和其他数据都按照一个指针的空间进行存储,使用强制类型转换把参数的类型转换为void *后再执行push函数,top函数返回的值再转换为原先的类型。为了堆栈也适用于较大的数据(例如结构),可以在堆栈中存储指向数据的指针。
    警告:
    这种方式的问题是它绕过了类型检查。我们没有办法证实传递给push函数的值正好是堆栈所使用的正确类型。如果一个整数意外地压入到一个元素类型为指针的堆栈中,其结果几乎肯定是一场灾难。
    使树模块与类型无关更为重要一些,因为树函数必须比较树节点的值。但是,我们可以向每个树函数传递一个指向由用户编写的比较函数的指针。同样,传递一个错误的指针也会造成灾难性的后果。
    17.5.3 名字冲突
    堆栈和队列模块都拥有is_full和is_empty函数,队列和树模块都拥有insert函数。如果需要向树模块增加一个delete函数,它就会与原先存在于队列模块中的delete函数发生冲突。
    为了使它们共存于程序中,所有这些函数的名字都必须是独一无二的。但是,人们有一种强烈的愿望,即在尽可能的情况下,让那些和每个数据结构关联的函数都保持“标准”名字。这个问题的解决方式是一种妥协方案:选择一种命名约定,使它既可以为人们所接受又能保证唯一性。例如,is_queue_empty和is_stack_empty名字解决了这个问题。它们的不利之处在于这些长名字使用起来不太方便,并且并未传递任何附加信息。
    17.5.4 标准函数库的ADT
    计算机科学虽然不是一门古老的科学,但我们对它的研究显然已经花费了相当长的时间,对堆栈和队列的行为的方方面面已经研究得相当透彻了。那么,为什么每个人还需要自己编写堆栈和队列函数呢?为什么这些ADT不是标准函数库的一部分呢?原因正是我们刚刚讨论过的3个问题。名字冲突很容易解决,但是,类型安全性的缺乏以及让用户直接操纵数据的危险性使得用一种通用而又安全的方式编写实现堆栈的库函数变得极不可行。
    解决这个问题就要求实现泛型(genericity),它是一种编写一组函数,但数据的类型暂时可以不确定的能力。这组函数随后用用户需要的不同类型进行实例化(instantiated)或创建。C语言并未提供这种能力,但可以使用#define定义近似地模仿这种机制。
    程序17.10a包含了一个#define宏,它的宏体是一个数组堆栈的完整实现。这个#define宏的参数是需要存储的值的类型、一个后缀以及需要使用的数组长度。后缀粘贴到由实现定义的每个函数名的后面,用于避免名字冲突。
    程序17.10b使用程序10.7a的声明创建两个堆栈,一个可以容纳10个整数,另一个可以容纳5个浮点数。当每个#define宏被扩展时,会创建一组新的堆栈函数,用于操作适当类型的数据。但是,如果需要两个整数堆栈,这种方法将会创建两组相同的函数。
    我们将程序17.10a进行改写,把它分为3个独立的宏:一个用于声明接口;一个用于创建操纵数据的函数;一个用于创建数据。当需要第一个整数堆栈时,所有3个宏均被使用。如果还需要另外的整数堆栈,可通过重复调用最后一个宏来实现。堆栈的接口也应该进行修改。函数必须接受一个附加的参数用于指定进行操作的堆栈。这些修改都留作练习。
    这个技巧使得创建泛型抽象数据类型库称为可能。但是,这种灵活性是要付出代价的。用户需要承担几个新的责任。
    1.采用一种命名约定,避免不同类型间堆栈的名字冲突。
    2.必须保证为每种不同类型的堆栈只创建一组堆栈函数。
    3.在访问堆栈时,必须保证使用适当的名字(例如,push_int或push_float等)。
    4.确保向函数传递正确的堆栈数据结构。
    毫无疑问的是,用C语言实现泛型是相当困难的,因为它的设计远早于泛型这个概念被提出之时。泛型是面向对象编程语言处理得比较完美的问题之一。

    /*
    **用静态数组实现一个泛型的堆栈。数组的长度在堆栈实例化时作为参数给出。
    */

    #include <assert.h>

    #define GENERIC_STACK( STACK_TYPE, SUFFIX, STACK_SIZE ) \
        static STACK_TYPE stack##SUFFIX[ STACK_SIZE ]; \
        static int          top_element##SUFFIX = -1; \
        \
        int \
        is_empty##SUFFIX( void ) \
        { \
            return top_element##SUFFIX == -1; \
        } \
        \
        int \
        is_full##SUFFIX( void ) \
        { \
            return top_element##SUFFIX == STACK_SIZE - 1; \
        } \
        \
        void \
        push##SUFFIX( STACK_TYPE value ) \
        { \
            assert( !is_full##SUFFIX() ); \
            top_element##SUFFIX += 1; \
            stack##SUFFIX[ top_element##SUFFIX ] = value; \
        } \
        \
        void \
        pop##SUFFIX( void ) \
        { \
            assert( !is_empty##SUFFIX() ); \
            top_element##SUFFIX -= 1; \
        } \
        \
        STACK_TYPE top##SUFFIX( void ) \
        { \
            assert( !is_empty##SUFFIX() ); \
            \
            return stack##SUFFIX[ top_element##SUFFIX ]; \
        }      
    程序17.10a 泛型数组堆栈    g_stack.h
    /*
    **一个使用泛型堆栈模块创建两个容纳不同类型数据的堆栈的用户程序。
    */
    #include <stdlib.h>
    #include <stdio.h>
    #include "g_stack.h"
    
    /*
    **创建两个堆栈,一个用于容纳整数,另一个用于容纳浮点值。
    */
    GENERIC_STACK( int, _int, 10 )
    GENERIC_STACK( float, _float, 5 )
    
    int 
    main( void ) {
        /*
        **往每个堆栈压入几个值
        */
        push_int( 5 );
        push_int( 22 );
        push_int( 15 );
        push_float( 25.3 );
        push_float( -40.5 );
        
        /*
        **清空整数堆栈并打印这些值
        */
        while( !is_empty_int() ){
            printf( "Popping %d\n", top_int() );
            pop_int();
        } 
        
        /*
        **清空浮点数堆栈并打印这些值。
        */
        while( !is_empty_float() ){
            printf( "Popping %f\n", top_float() );
            pop_float();
        } 
        
        return EXIT_SUCCESS;
    } 
    程序17.10b 使用泛型数组堆栈 g_client.c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_40186813

你的能量无可限量。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值