【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:泛型 (Generics),使用前置处理器 (Preprocessor)

【赞助商连结】

    在前文中,我们展示了使用指向 void 的指针来实现的泛型程序。在本文中,我们会展示以 C 前置处理器 (C 宏) 来实现的泛型程序。这个方法是非主流的手法,因为 (1) 没有类型安全,(2) 难以调试。这己经算是一种反模式 (anti-pattern),我们仍然展示这个方法,读者可自行决定要不要使用在自己的项目中。

    我们将完整的程序代码放在这里,有兴趣的读者可自行追踪,本文仅节录部分内容。

    我们先从外部程序来看如何使用此泛型队列:

    #include <assert.h>
    #include <stdbool.h>
    #include <stdio.h>
    #include "queue.h"
    
    // Declare queue function once per type.
    queue_declare(int);
    
    int main(void)
    {
        bool failed = false;
    
        // Queue: NULL
        queue_class(int) *q = queue_new(int,
            (queue_params(int)) { .item_free = NULL });
        if (!q) {
            perror("Failed to allocate queue q");
            return false;
        }
    
        // Queue: 9 -> NULL
        if (!queue_enqueue(int, q, 9)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 9 -> 5 -> NULL
        if (!queue_enqueue(int, q, 5)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 9 -> 5 -> 7 -> NULL
        if (!queue_enqueue(int, q, 7)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 5 -> 7 -> NULL
        if (queue_dequeue(int, q) != 9) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: 7 -> NULL
        if (queue_dequeue(int, q) != 5) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        // Queue: NULL
        if (queue_dequeue(int, q) != 7) {
            failed = true;
            goto QUEUE_FREE;
        }
    
        if (!queue_is_empty(int, q)) {
            failed = true;
            goto QUEUE_FREE;
        }
    
    QUEUE_FREE:
        queue_free(int, q);
    
        if (failed) {
            return 1;
        }
    
        return 0;
    }

    由此可见,我们不需要额外的 box class 就可以在基础类型中使用此泛型程序。附带一提,由于我们的「函式」是从宏扩张而来的,直接使用指针会引发错误,要额外加入以下类型定义:

    typedef struct klass * klass_p;

    之后再将 klass_p 做为类型声明,塞入我们的宏即可。

    我们来看 queue_declare 的实际内容:

    #define queue_declare(type) \
        queue_node_declare(type) \
        queue_class_declare(type) \
        queue_node_new_declare(type) \
        queue_params_declare(type) \
        queue_class_new_declare(type) \
        queue_class_free_declare(type) \
        queue_class_is_empty_declare(type) \
        queue_class_peek_declare(type) \
        queue_class_enqueue_declare(type) \
        queue_class_dequeue_declare(type)

    由此可知,其实 queue_declare 只是一个声明其他宏的宏。

    以下是一些「函式」的声明:

    #define queue_new(type, item_free) \
        queue_##type##_new(item_free)
    
    #define queue_free(type, self) \
        queue_##type##_free(self)
    
    #define queue_is_empty(type, self) \
        queue_##type##_is_empty(self)
    
    #define queue_peek(type, self) \
        queue_##type##_peek(self)
    
    #define queue_enqueue(type, self, data) \
        queue_##type##_enqueue(self, data)
    
    #define queue_dequeue(type, self) \
        queue_##type##_dequeue(self)

    在泛型程序中,我们不能将类型预先写死,所以要由宏使用者提供类型资讯,在宏中会扩张成相对应的函式。

    以下是宏版本的类声明:

    #define queue_class(type) queue_##type
    
    #define queue_class_declare(type) \
        typedef struct queue_##type##_s queue_class(type); \
        struct queue_##type##_s { \
            freeFn item_free; \
            queue_node(type) *head; \
            queue_node(type) *tail; \
        };

    仔细观察,可发现本质上仍是队列类,只是把类型的地方在编译时代换掉。

    以下是宏版本的建构函式:

    #define queue_class_new_declare(type) \
        queue_class(type) * queue_##type##_new(queue_params(type) params) \
        { \
            queue_class(type) * q = malloc(sizeof(queue_class(type))); \
            if (!q) { \
                return q; \
            } \
            q->item_free = params.item_free; \
            q->head = NULL; \
            q->tail = NULL; \
            return q; \
        }

    由于是宏的缘故,程序代码会比一般的建构函式难阅读一些。

    以下是宏版本的解构函式:

    #define queue_class_free_declare(type) \
        void queue_##type##_free(void *self) \
        { \
            if (!self) { \
                return; \
            } \
            queue_node(type) *curr = ((queue_class(type) *) self)->head; \
            freeFn fn = ((queue_class(type) *) self)->item_free; \
            queue_node(type) *temp; \
            while (curr) { \
                temp = curr; \
                curr = curr->next; \
                if (fn) { \
                    fn(temp->data); \
                } \
                free(temp); \
            } \
            free(self); \
        }

    在节点内的数据是基础类型时,不需要手动释放内存,但该数据是指针类型时则要。所以本程序检查 fn 是否存在,当 fn 存在时调用该函式把内存放掉。

    以下是从将数据推入队列前端的宏:

    #define queue_class_enqueue_declare(type) \
        bool queue_##type##_enqueue(queue_class(type) *self, type data) \
        { \
            assert(self); \
            queue_node(type) *node = queue_node_new(type, data); \
            if (!node) { \
                return false; \
            } \
            if (!(self->tail)) { \
                self->head = node; \
                self->tail = node; \
                return true; \
            } \
            self->tail->next = node; \
            node->prev = self->tail; \
            self->tail = node; \
            return true; \
        }

    其实和一般的队列函式相差不大。

    以下是将数据从队列前端移出的宏:

    #define queue_class_dequeue_declare(type) \
        type queue_##type##_dequeue(queue_class(type) *self) \
        { \
            assert(self); \
            if (self->head == self->tail) { \
                type popped = self->head->data; \
                free(self->head); \
                self->head = NULL; \
                self->tail = NULL; \
                return popped; \
            } \
            queue_node(type) *curr = self->head; \
            type popped = curr->data; \
            self->head = curr->next; \
            free(curr); \
            return popped; \
        }

    在此处,我们没有拷贝节点内的数据,如果碰到指针类型时,宏使用者要自行负责释放该数据。

    稍微想一下前置处理器的工作方式,就会知道这样的「函式库」实用性偏低。在宏扩张后,原本的程序已经被代换掉了,程序的行数可能和原本的程序代码有相当的差距,我们无法直接从编译器的错误讯息得知到底是在原程序的那一行出错;此外,如果实际写过一些宏程序就知道 C 宏容易出错且调试相对困难;这样的程序也缺乏静态类型语言应有的类型安全。虽然我们可以用 C 前置处理器写泛型程序,但应谨慎为之。

    【赞助商连结】
    TAGS: C 语言