C和指针学习总结之泛型数组堆栈

24 篇文章 2 订阅
8 篇文章 0 订阅

为什么要使用泛型编程?

  1. 拥有超过一个堆栈
    最主要的一个问题是它们把用于保存结构的内存和那些用于操纵它们的函数都封装在一起了。这样一来,一个程序便不能拥有超过一个的堆栈。
    这个限制容易解决,只要从堆栈的实现模块中去除数组和top_element的声明,并把它们放入用户代码即可。然后,它们通过参数被堆栈函数访问,这些函数便不再固定于某个数组。用户可以创建任意数量的数组,并通过调用堆栈函数将它们作为堆栈使用。

  2. 拥有超过一种的类型
    即使前面的问题得以解决,存储于堆栈的值的类型在编译时便已固定,它就是stack.h头文件中所定义的类型。
    解决这个问题最简单的方法是另外编写一份堆栈函数的拷贝,用于处理不同的数据类型。这个方法可以达到目的,但它涉及大量重复代码,使程序的维护工作变得困难。
    一种更为优雅的方法是把整个堆栈模块实现为一个#define宏,把目标类型作为参数。这个定义然后便可以用于创建每种目标类型的堆栈函数。但是,为了使这种方案得以运作,必须找到一种方法为不同类型的堆栈函数产生独一无二的函数名,这样它们相互之间就不会冲突。同时,你必须小心在意,每种类型只能创建一组函数,不管你实际需要多少个这种类型的堆栈。
    第三种方法是使堆栈与类型无关,方法是让它存储void *类型的值。将整数和其他数据都按照一个指针的空间进行存储,使用强制类型转换把参数的类型转换为void *再执行push函数,top函数返回的值再转换回原先的类型。为了使堆栈也适用于较大的数组(例如结构),你可以在堆栈中存储指向数据的指针。

  3. 名字冲突
    堆栈和队列模块都拥有is_full和is_empty函数,队列和树模块拥有insert函数。如果你需要向树模块增加一个delete函数,它就会与原先存在于队列模块的delete函数发生冲突。为了使它们共存于程序中,所有这些函数的名字都必须是独一无二的。

  4. 标准函数库的ADT
    为什么每个人还需要自己编写堆栈和队列函数呢?为什么这些ADT不是标准函数库的一部分呢?
    其原因正是我们刚刚讨论过的三个问题。名字冲突问题很容易解决,但是,类型安全型的缺乏以及让用户直接操纵数据的危险性使得用一种通用而又安全的方式编写实现堆栈的库函数变得极不可行。
    解决这个问题就要求实现泛型,它是一种编写一组函数,但数据的类型暂时可以不确定的能力。这组函数随后用用户需要的不同类型进行实例化或创建。C语言并未提供这种能力,但我们可以使用#define定义近似地模拟这种机制。

g_stack.h

程序g_stack.h包含了一个#define宏,它的宏体是一个数组堆栈的完整实现。这个#define宏的参数是需要存储的值的类型、一个后缀以及需要使用的数组长度。后缀用于粘贴到由实现定义的每个函数名的后面,用于避免名字冲突。

/*
 * Implement a generic stack with static arrays. The length of the array is given
 * as a parameter when the stack is instantiated.
 */
#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];      \
        }

g_client.c

程序g_client.c使用程序g_stack.h的声明创建两个堆栈,一个可以容纳10个整数,另一个可以容纳5个浮点数。当每个#define宏被扩展时,一组新的堆栈函数被创建,用于操作适当类型的数据。但是,如果需要两个整数堆栈,这种方法将会创建两组相同的函数。
这个技巧使得创建泛型抽象数据类型库成为可能。但是,这种灵活性是要付出代价的。用户需要承担几个新的责任。现在,他必须:

  1. 采用一种命名约定,避免不同类型间堆栈的名字冲突。
  2. 必须保证为每种不同类型的堆栈只创建一组堆栈函数。
  3. 在访问堆栈时,必须保证使用适当的名字(例如,push_int或push_float等)。
  4. 确保向函数传递正确的堆栈数据结构。
/*
 * A user program that uses the generic stack module to create two stacks that
 * hold different types of data.
 */
#include <stdlib.h>
#include <stdio.h>
#include "g_stack.h"

/*
 * Create two stacks, one to hold integers and the other to hold floating point numbers.
 */
GENERIC_STACK(int, _int, 10)
GENERIC_STACK(float, _float, 5)

int main()
{
    /*
     * Push a few values into each stack.
     */
    push_int(5);
    push_int(22);
    push_int(15);
    push_float(25.3);
    push_float(-40.5);

    /*
     * Clear the integer stack and print the values.
     */
    while (!is_empty_int()){
        printf("Popping %d\n", top_int());
        pop_int();
    }

    /*
     * Clear the floating-point stack and print the values.
     */
    while (!is_empty_float()){
        printf("Popping %f\n", top_float());
        pop_float();
    }

    return EXIT_SUCCESS;
}

毫不吃惊的是,用C语言实现泛型是相当困难的,因为它的设计远早于泛型这个概念被提出之时。泛型是面向对象编程语言处理得比较完美的问题之一。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程小老弟

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值