系统程序员成长计划-组合的威力

在《设计模式-可复用面向对象软件的基础》的序言里提到软件设计的两个基本原则:

针对接口编程,而不是针对实现编程。接口是抽象的,因为抽象所以简单。接口是对象的本质,因为是本质所以稳定。接口是降低复杂度和隔离变化的有力武器,C语言里没有接口的概念,不是因为接口不重要,而是C语言把它视为理所当然的东西(函数指针无所不在), 正所谓玫瑰不叫玫瑰,依然芳香如故。在前面我们已经看到,C语言里的接口是相当直观和优雅的。

优先使用组合,而不是类继承。与组合相比,继承更为复杂,特别是多重继承和多层继承,甚至会带来一些歧义,给理解上造成困难。与组合相比,继承缺乏灵活性,继承在编译时就绑定了父类和子类之间的关系,而组合却可以在运行时动态改变。C语言没有继承的概念(当然可以实现继承),自然大量使用组合来构建大型系统,这在客观上恰恰与设计原则是一致的。

本章节将借助队列,栈和哈表来练习这种基本的重用方法。请读者实现队列,栈和哈表,要求重用前面的链表实现。


队列

队列是一种很常用的数据,操作系统用队列来管理运行的进程,驱动程序用队列来管理要传输的数据包,GUI框架用队列来管理各种GUI事件。队列是一种先进先出(FIFO, First in First out)的数据结构,数据只能从队列头取,向队列尾追加。队列的名称很形象的表达了它的意义,就像排队上车一样,前面的先上,后来的站在后面排队。

队列主要的接口函数有:

o 创建队列:queue_create
o 取队列头的元素:queue_head
o 追加一个元素到队列尾:queue_push
o 删除队列头元素:queue_pop
o 取队列中元素个数(同时也可以判断队列是否为空):queue_length
o 遍历队列中的元素:queue_foreach;
o 销毁队列 queue_destroy

队列看起来比链表和数组要高级一点,其实它只不过是链表和数组的一种特殊形式而已,下面我们重用链表来实现队列:

o 队列的数据结构

struct _Queue
{
    DList* dlist;
};

这里队列只是对双向链表的一个包装。

o 创建队列

Queue* queue_create(DataDestroyFunc data_destroy, void* ctx)
{
    Queue* thiz = (Queue*)malloc(sizeof(Queue));

    if(thiz != NULL)
    {
        if((thiz->dlist = dlist_create(data_destroy, ctx)) == NULL)
        {
            free(thiz);
            thiz = NULL;
        }
    }

    return thiz;
}

创建队列时,除了分配自己的空间外,就是简单的创建一个双向链表。

o 取队列头的元素

Ret      queue_head(Queue* thiz, void** data)
{
    return_val_if_fail(thiz != NULL && data != NULL, RET_INVALID_PARAMS);

    return dlist_get_by_index(thiz->dlist, 0, data);
}

我们认为链表的第一个元素是队列头,取队列头的元素就是取链表的第一个元素。

o 追加一个元素到队列尾

Ret      queue_push(Queue* thiz, void* data)
{
    return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

    return dlist_append(thiz->dlist, data);
}

我们认为链表的最后一个元素是队列尾,追加一个元素到队列尾就是追加一个元素到链表尾。

o 删除队列头元素

Ret      queue_pop(Queue* thiz)
{
    return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

    return dlist_delete(thiz->dlist, 0);
}

删除队列头的元素就是删除链表的第一个元素。

o 取队列中元素个数

size_t   queue_length(Queue* thiz)
{
    return_val_if_fail(thiz != NULL, 0);

    return dlist_length(thiz->dlist);
}

队列中元素的个数等同于链表元素的个数。

o 遍历队列中的元素

Ret      queue_foreach(Queue* thiz, DataVisitFunc visit, void* ctx)
{
    return_val_if_fail(thiz != NULL && visit != NULL, RET_INVALID_PARAMS);

    return dlist_foreach(thiz->dlist, visit, ctx);
}

遍历队列中的元素等同于遍历链表中的元素。

o 销毁队列

void queue_destroy(Queue* thiz)
{
    if(thiz != NULL)
    {
        dlist_destroy(thiz->dlist);
        thiz->dlist = NULL;

        free(thiz);
    }

    return;
}

销毁链表然后释放自身的空间。

用组合的方式实现队列很简单吧,不用半小时就可以写出来。虽然链表已经通过了自动测试,队列的自动测试也不能省,在这里我们就不列出代码了。

上面我们实现的是通用队列,除了通用队列外,还有几种特殊队列应用也非常广泛:

o 优先级队列。它的不同之外在于,插入元素时,不必追加到队尾,而是根据元素的优先级插到队列的适当位置。这个就像排队上车时,后面来了个老人,大家让他先上一样的。内核的进程管理器通常采用估先级队列管理进程。

o 循环队列。循环队列的特点是最大元素个数是固定的。这个我们在前面学习同步时已经提过,它的优点是两个并发的实体(线程或进程),按一个读一个写的方式访问,不需要加锁。设备驱动程序经常使用循环队列来管理数据包。

栈是一种后进先出(LIFO, last in first out)的数据结构,与队列的先进先出(FIFO)相比,这种规则似乎不太公平,计算机可不管这个。事实上,栈是最重要的数据结构之一:没有栈,基于下推自动机的编译器不能工作,我们只能写汇编程序。没有栈,无法实现递归/多级函数调用,程序根本就无法工作。

栈主要的接口函数有:

o 创建栈 stack_create
o 取栈顶元素 stack_top
o 放入元素到栈顶stack_push
o 删栈栈顶元素stack_pop
o 取栈中元素个数 stack_length
o 遍历栈中的元素stack_foreach
o 销毁栈 stack_destroy

栈同样是链表和数组的一种特殊形式而已,下面我们重用链表来实现栈:

o 栈的数据结构

struct _Stack
{
    DList* dlist;
};

这里和队列的数据结构一样,由一个链表组成。

o 创建栈

Stack* stack_create(DataDestroyFunc data_destroy, void* ctx)
{
    Stack* thiz = (Stack*)malloc(sizeof(Stack));

    if(thiz != NULL)
    {
        if((thiz->dlist = dlist_create(data_destroy, ctx)) == NULL)
        {
            free(thiz);
            thiz = NULL;
        }
    }

    return thiz;
}

创建栈时,除了分配自己的空间外,就是简单的创建一个双向链表。

o 取栈顶元素

Ret      stack_top(Stack* thiz, void** data)
{
    return_val_if_fail(thiz != NULL && data != NULL, RET_INVALID_PARAMS);

    return dlist_get_by_index(thiz->dlist, 0, data);
}

我们认为链表的第一个元素是栈顶,取栈顶的元素就是取链表的第一个元素。

o 放入元素到栈顶

Ret      stack_push(Stack* thiz, void* data)
{
    return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

    return dlist_prepend(thiz->dlist, data);
}

放入元素到栈顶就是插入一个元素到链表头。

o 删栈栈顶元素

Ret      stack_pop(Stack* thiz)
{
    return_val_if_fail(thiz != NULL, RET_INVALID_PARAMS);

    return dlist_delete(thiz->dlist, 0);
}

删栈栈顶元素就是删除链表的第一个元素。

o 取栈中元素个数

size_t   stack_length(Stack* thiz)
{
    return_val_if_fail(thiz != NULL, 0);

    return dlist_length(thiz->dlist);
}

栈中的个数等同于链表的个数。

o 遍历栈中的元素

Ret      stack_foreach(Stack* thiz, DataVisitFunc visit, void* ctx)
{
    return_val_if_fail(thiz != NULL && visit != NULL, RET_INVALID_PARAMS);

    return dlist_foreach(thiz->dlist, visit, ctx);
}

遍历栈中的元素等同于遍历链表中的元素。

o 销毁栈

void stack_destroy(Stack* thiz)
{
    if(thiz != NULL)
    {
        dlist_destroy(thiz->dlist);
        thiz->dlist = NULL;

        free(thiz);
    }

    return;
}

销毁双向链表然后释放自身的空间。

栈是一个非常重要的数据,但奇怪的是我们很少有机会去写它。事实上,我从来没有在工作中写过一个栈。这是怎么回事呢?原因是我们的计算机本身是基于栈的,很多事情计算机已经在我们不知道的情况下帮我们处理了,比如函数调用(特殊是递归调用),计算机帮我们处理了。用递归下降进行的语法分析利用了函数调用的递归性,也不需要显式的构造栈。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值