【问题标题】:Generic linked list print and add by value in C通用链表在C中按值打印和添加
【发布时间】:2021-03-25 07:47:07
【问题描述】:

我的目标是写一个简单的通用链表。

  1. 我不知道如何创建一个完美的 listPrint 函数,因为 printf 必须获取该值的类型,而且我必须将 ptr->data 转换为适当的格式。目前我只能打印intprintf("%i ",*((int*)ptr->data))。我如何打印任何类型的值,例如int,float,char..

  2. 当我推回列表时,我必须创建一个int* i,然后是listPushBack(list,&i),但我想这样做listPushBack(list,101)。我知道如果我做这样的事情我必须这样做,但是我应该在我的代码中更改什么以允许我listPushBack(list,101)

存储在给定列表中的元素将是相同类型(int 列表、double 列表、struct WhatEver* 列表等)

list.c

struct node {
    void* data;
    struct node* next;
    struct node* prev;
    size_t typeSize;
};
struct list {
    struct node* head;
    struct node* tail;
    size_t elementSize;
};
node* nCreate(void* data, size_t typeSize) {
    node* nNode = malloc(sizeof(node));
    nNode->data = malloc(typeSize);
    memcpy(nNode->data, data, typeSize);
    nNode->prev = nNode->next = NULL;
    nNode->typeSize = typeSize;
    return nNode;
}
list* listCreate(size_t typeSize) {
    list* l = malloc(sizeof(l));
    l->head = l->tail = NULL;
    l->elementSize = typeSize;
    return l;
}
void listPushBack(list* l, void* val) {
    node* newNode = nCreate(val,l->elementSize);
    ...
}
void listPrint(list* l) {
    node* ptr = l->head;
    do {
        printf("%i ",*((int*)ptr->data));
    } while((ptr = ptr->next) != NULL);
    printf("\n");
}

主要

int main() {
    list* list = listCreate(sizeof(int));
    int* i = 10;
    listPushBack(list,&i);
    listPrint(list);
    return 0;
}

【问题讨论】:

  • listCreate 中,您不会转换malloc 的返回值,但在nCreate 中您可以。你在写 C 还是 C++?重要的是后者 (C++) 需要强制转换,而在前者 (C) 中,malloc 的强制转换返回被认为是错误的形式/错误。您似乎还使用void *data 在列表中存储您想要的任何内容。这很好,但如果你最终要将ptr->data 转换为一个int 指针,为什么还要麻烦void * 呢?如果您只处理少数类型,枚举和联合可能会更好
  • nNode->data = malloc(typesize); 后跟 nNode->data = data; 是内存泄漏。它分配存储空间,然后“忘记”指向存储空间的指针。我认为您希望 nNode->data = malloc(typesize); 后跟 memcpy(nNode->data, data, typesize); 将内存内容复制到节点。
  • @SYZYGY:我对 malloc 演员表的评论提到了声明 node* nNode = (node*)malloc(sizeof(node));。如果您正在编写 C,则应该删除该演员表。
  • 您将无法在 C 中编写一个完全“通用”的列表,因为 C 不像 C++ 那样提供类型重载或运算符重载。至少您可以为列表中的泛型类型提供存储并提供泛型列表操作(adddelete_listtraverse 等),但任何需要特定类型定义的函数(compare、@987654351 @、delete_node 等)必须通过函数指针编写和传递,就像 compareqsort 一样。
  • 您是否期望存储在给定列表中的元素将是相同类型(int 列表、double 列表、struct WhatEver * 列表等),或者元素可以列表中有异构类型?

标签: c list linked-list


【解决方案1】:

对于第一点,恐怕除了将类型存储在列表中并在打印时使用开关之外没有其他解决方案。对于第二点,一种可能的解决方案是使用联合而不是 void(当然,您可以在联合和计算类型大小的函数中添加所需的任何类型):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

 union Value {
    int i;
    long long l;
    float f;
    double d;
};

static inline size_t sizeForType(char type) {
    switch(type) {
        case 'i': case 'f':  return 4;
        case 'l': case 'd': return 8;
        default: return 0;
    }
}

struct node {
    union Value data;
    struct node* next;
    struct node* prev;
};
struct list {
    struct node* head;
    struct node* tail;
    char type;
};
struct node* nCreate(union Value data, char type) {
    struct node* nNode = malloc(sizeof(*nNode));
    size_t size = sizeForType(type);
    nNode->data = data;
    nNode->prev = nNode->next = NULL;
    return nNode;
}

struct list* listCreate(char type) {
    struct list* l = malloc(sizeof(*l));
    l->head = l->tail = NULL;
    l->type = type;
    return l;
}

void listPushBack(struct list* l, union Value data) {
    struct node* newNode = nCreate(data, l->type);
    if (!l->head) l->head = newNode;
    if (!l->tail) l->tail = newNode;
    else {
        newNode->prev = l->tail;
        l->tail->next = newNode;
        l->tail = newNode;
    }
}

void listPrint(struct list* l) {
    struct node* ptr = l->head;
    if (!ptr) { printf("Empty list\n"); return; }
    do {
        switch(l->type) {
            case 'i': printf("%d ", ptr->data.i);    break;
            case 'l': printf("%lld ", ptr->data.l);  break;
            case 'f': printf("%f ", ptr->data.f);    break;
            case 'd': printf("%f ", ptr->data.d);    break;
        }
    } while((ptr = ptr->next) != NULL);
    printf("\n");
}

int main() {
    struct list* list = listCreate('i');
    union Value v = { .i = 10 };
    listPushBack(list, v);
    v.i = 11;
    listPushBack(list, v);
    v.i = 12;
    listPushBack(list, v);
    listPrint(list); //==> 10, 11, 12
    return 0;
}

不要忘记释放分配的内存,我没有这样做。

【讨论】:

  • “将类型存储在列表中并使用开关”。这与“通用”相反。
【解决方案2】:

关于打印列表: 您可以允许您的打印函数接收打印数据项的函数指针,或者至少生成它的 c 字符串样式表示。

这种方法并不少见。事实上,Gnome 的 glib 就是使用这种类型的系统。这是他们的哈希表 API 的一个示例: https://developer.gnome.org/glib/stable/glib-Hash-Tables.html

注意他们如何广泛使用函数指针。

您也可以将此模式应用于您的推送场景。

总结一下: 列表节点只需要一个 void *data 成员。可以使用辅助函数处理类型信息。

编辑 1: 我没有正确阅读您的第二点,所以我想补充说明以防万一。列表将始终将数据存储为 (void *),这意味着两件事:(1) 列表永远不会知道正在存储什么数据,(2) 调用者必须将数据转换为 (void *)。由于使用了列表处理函数可以使用的辅助函数指针,因此这不是问题。

【讨论】:

    【解决方案3】:

    对于打印,您可以在struct list 中包含一个函数指针,可以调用它来打印元素。这可以设置为用户定义的函数,该函数将知道如何打印该特定类型。

    要执行listPushBack(list,101) 之类的操作,您需要创建一个包装函数,即listPushBackInt,它将int 作为第二个参数,然后将该参数的地址传递给listPushBack

    所以现在您的列表结构将如下所示:

    struct list {
        struct node* head;
        struct node* tail;
        size_t elementSize;
        void (*elementPrint)(void *);
    };
    

    您的打印功能会这样做:

    void listPrint(list* l) {
        node* ptr = l->head;
        do {
            l->elementPrint(ptr->data);   // use function pointer
        } while((ptr = ptr->next) != NULL);
        printf("\n");
    }
    

    现在您的列表创建函数将接受一个参数来设置函数指针:

    list* listCreate(size_t typeSize, void (*print)(void *)) {
        list* l = malloc(sizeof(l));
        l->head = l->tail = NULL;
        l->elementSize = typeSize;
        l->elementPrint = print;   // set function pointer
        return l;
    }
    

    现在要创建int的列表,首先要创建打印函数:

    void printInt(void *data)
    {
        printf("%i ", *(int*)data);
    }
    

    然后你创建这样的列表:

    listCreate(sizeof(int), printInt);
    

    然后你可以创建这个包装函数:

    void listPushBackInt(list* l, int val)
    {
        listPushBack(l, &val);
    }
    

    要像这样插入int 值:

    listPushBackInt(l, 101)
    

    【讨论】:

    • 好答案。一个很好的增强是一个带有宏的头文件,以在给定类型和 printf 格式字符串的情况下生成 printXXX 和 listPushBackXXX(和其他函数)。例如,MAKE_LIST_TYPE(int,"%i")、MAKE_LIST_TYPE(double,"%g") 等
    • 这种方法的一些限制是:(1) 它仍然不是通用的,因为您有一个特定于 Int 的 API,以及 (2) 用户可能会认为调用 listPushBackInt 会导致在为该数据分配的内存中。例如,用户可能会执行 listPushBackInt(&mylist, x),其中 x 是某个局部作用域为函数的变量,这是不正确的。如果用户执行 listPushBackInt(1) 会发生什么?
    • @kprow listPushBackInt 函数将是用户定义的 listPushBack 包装器,就像 printInt 回调一样。这不是严格需要的。另请注意,nCreate 实际上确实为该值分配内存并使用memcpy 将值复制到其中。listPushBackInt(1) 会导致编译器错误,因此这不是问题。
    【解决方案4】:

    序言

    我这样做的方式是使用函数指针和一个“应用”函数,该函数给出一个列表、一个函数指针和一些(通用)数据,这些数据与指向数据的指针一起传递给函数指针一个列表节点。

    apply 函数解决了如何打印列表内容的问题(尽管下面显示的代码有两个 apply 函数,用于以列表的正序和逆序应用数据)。

    您询问是否能够提供值而不是必须将指针传递给listPushBack() 函数。除了为每种类型提供覆盖函数之外,没有简单的方法可以做到这一点。因此,您可以:

    static inline void listPushBack_int(list *lp, int i)
    {
        listPushBack(lp, &i);
    }
    

    对于任何其他类型也是如此。

    问题代码中的错误

    请注意,问题中的代码在内存分配中存在错误 - 在listCreate() 中,您有:

    list* l = malloc(sizeof(l));
    

    你需要:

    list *l = malloc(sizeof(*l));
    

    genlist.h

    这是标题。注意struct list 类型,又名list,是不透明的;使用标头的代码对结构类型的内部一无所知,所有访问都是通过代码中定义的函数进行的。我将listPushBack() 重命名为listPushTail() 并添加了listPushHead()。我添加了listApplyForward()listApplyReverse()listDestroy()。我删除了listPrint(),因为它不是通用的,不能轻易地被通用化。

    #ifndef GENLIST_H_INCLUDED
    #define GENLIST_H_INCLUDED
    
    #include <stddef.h>         /* size_t */
    
    typedef struct list list;
    
    typedef void (*Apply)(const void *vp, void *thunk);
    
    extern list *listCreate(size_t typeSize);
    extern void listDestroy(list *lp);
    
    extern void listPushHead(list *lp, void *val);
    extern void listPushTail(list *lp, void *val);
    
    extern void listApplyForward(const list *lp, Apply apply, void *thunk);
    extern void listApplyReverse(const list *lp, Apply apply, void *thunk);
    
    #endif /* GENLIST_H_INCLUDED */
    

    这显然不是一个完整的列表界面——它是一组非常简单的函数。它没有代码来访问列表中的单个元素,或者检索列表中的值,或者从列表中删除值,或者......但是,可以根据需要设计和添加这些功能。

    genlist.c

    节点类型也完全隐藏,因此nodeCreate() 函数(从nCreate() 重命名)是静态的。可以说,也应该有一个 nodeDestroy() 函数——但它被包含在 listDestroy() 内的代码中。

    #include "genlist.h"
    #include <assert.h>     /* DEBUG */
    
    #ifdef DEBUG
    #include <inttypes.h>   /* DEBUG */
    #include <stdio.h>      /* DEBUG */
    #endif /* DEBUG */
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct node node;
    
    struct node
    {
        void    *data;
        node    *next;
        node    *prev;
    };
    
    struct list
    {
        node    *head;
        node    *tail;
        size_t  elementSize;
    };
    
    static node *nodeCreate(void *data, size_t typeSize)
    {
        node *np = malloc(sizeof(node));
        assert(np != NULL);
        np->data = malloc(typeSize);
        assert(np->data != NULL);
        memcpy(np->data, data, typeSize);
        np->prev = np->next = NULL;
    #ifdef DEBUG
        printf("Node: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
                typeSize, (uintptr_t)np, (uintptr_t)np->prev, (uintptr_t)np->next);
        fflush(stdout);
    #endif /* DEBUG */
        return np;
    }
    
    list *listCreate(size_t typeSize)
    {
        list *lp = malloc(sizeof(*lp));
        assert(lp != NULL);
        lp->head = lp->tail = NULL;
        lp->elementSize = typeSize;
    #ifdef DEBUG
        printf("List: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
                lp->elementSize, (uintptr_t)lp, (uintptr_t)lp->head, (uintptr_t)lp->tail);
        fflush(stdout);
    #endif /* DEBUG */
        return lp;
    }
    
    void listDestroy(list *lp)
    {
        assert(lp != NULL);
    #ifdef DEBUG
        printf("Destroy List: %zu: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
                lp->elementSize, (uintptr_t)lp, (uintptr_t)lp->head, (uintptr_t)lp->tail);
        fflush(stdout);
    #endif /* DEBUG */
        node *np = lp->head;
        while (np != NULL)
        {
    #ifdef DEBUG
            printf("Destroy Node: %.12" PRIXPTR ",%.12" PRIXPTR ",%.12" PRIXPTR "\n",
                    (uintptr_t)np, (uintptr_t)np->prev, (uintptr_t)np->next);
            fflush(stdout);
    #endif /* DEBUG */
            node *next = np->next;
            free(np->data);
            free(np);
            np = next;
        }
        free(lp);
    }
    
    void listPushTail(list *lp, void *val)
    {
        node *newNode = nodeCreate(val, lp->elementSize);
        if (lp->head == NULL)
        {
            lp->head = newNode;
            lp->tail = newNode;
        }
        else
        {
            lp->tail->next = newNode;
            newNode->prev = lp->tail;
            lp->tail = newNode;
        }
    }
    
    void listPushHead(list *lp, void *val)
    {
        node *newNode = nodeCreate(val, lp->elementSize);
        if (lp->head == NULL)
        {
            lp->head = newNode;
            lp->tail = newNode;
        }
        else
        {
            lp->head->prev = newNode;
            newNode->next = lp->head;
            lp->head = newNode;
        }
    }
    
    void listApplyForward(const list *lp, Apply apply, void *thunk)
    {
        node *ptr = lp->head;
        while (ptr != NULL)
        {
            // apply(ptr->data, thunk);
            (*apply)(ptr->data, thunk);
            ptr = ptr->next;
        }
    }
    
    void listApplyReverse(const list *lp, Apply apply, void *thunk)
    {
        node *ptr = lp->tail;
        while (ptr != NULL)
        {
            // apply(ptr->data, thunk);
            (*apply)(ptr->data, thunk);
            ptr = ptr->prev;
        }
    }
    

    “apply”函数以外的函数是双向链表的直接实现,空指针标记列表的末尾(其他设计也是可能的,例如使用循环列表)。

    函数listApplyForward()listApplyReverse() 遍历列表,调用apply 参数指定的函数,并带有指向当前节点数据的指针和thunk——指向任意数据的指针被调用的函数知道如何处理。 ('thunk' 的字典定义没有帮助;Thunk 的维基百科定义更接近目标,但适用于函数(子例程)而不是数据。但是,(macOS 和)FreeBSD 的手册页 @987654322 @ 使用名称 thunk 与我使用的含义相同——传递给回调函数的通用数据指针。)

    当通过函数指针调用函数时,我仍然更喜欢显式的预标准符号(*pointer)(arg1, …)。最近学习 C 的人可能更喜欢使用pointer(arg1, …)。当我学习 C 时,这种表示法不起作用。

    该代码包含一些我在追踪分配错误问题时使用的调试代码。可以说,打印应该是stderr而不是stdout,但添加fflush(stdout)就足够了。

    请注意,此代码假定您可以安全地按位复制数据而不会遇到所有权问题。这意味着它可以适用于不包含指针的结构,例如 - 但如果复制的数据包含指向动态分配内存的指针,或指向调用函数中的局部变量,甚至指向结构内位置的指针,则会严重失败.您可以设计更复杂的方案来复制数据。例如,您可以将“复制构造函数”函数指针和“析构函数”传递给列表创建函数,并将它们记录在列表结构中,并在将节点添加到列表或从列表中删除节点等时使用它们。 OTOH,它们通常是不必要的——按位复制通常适用于嵌入在列表中的结构——因此对于需要或多或少内存管理的数据类型,您最终可能会得到几个“通用列表”类型。

    main.c

    此代码使用genlist.[ch] 中的函数的公共接口。它创建了一个int 列表,并使用函数print_int() 打印数据,并使用另一个函数sum_int() 显示如何对列表中的元素求和。它还创建了double 的列表,并使用函数print_dbl() 打印数据,并计算value1 / value2 * value3 / value … 的值,主要是为了演示用于控制计算的非平凡用户定义结构类型。根据列表是向前还是向后遍历,这会产生不同的答案。

    代码使用rand() 生成随机数。它刻意不使用srand(time(0))或等价物来改变顺序——对于调试工作来说,每次相同数字的稳定性更有价值。

    #include "genlist.h"
    #include <stdio.h>
    #include <stdlib.h>
    #include <inttypes.h>
    
    static void print_int(const void *data, void *thunk)
    {
        FILE *fp = thunk;
        fprintf(fp, " %d", *(int *)data);
    }
    
    static void print_dbl(const void *data, void *thunk)
    {
        FILE *fp = thunk;
        fprintf(fp, " %7.3f", *(double *)data);
    }
    
    static void int_sum(const void *data, void *thunk)
    {
        int *sum = thunk;
        *sum += *(int *)data;
    }
    
    typedef struct DblFun
    {
        double val;
        int    idx;
    } DblFun;
    
    static void dbl_fun(const void *data, void *thunk)
    {
        DblFun *dp = thunk;
        double val = *(double *)data;
        if (dp->idx == 0)
            dp->val = val;
        else if (dp->idx % 2 == 1)
            dp->val /= val;
        else
            dp->val *= val;
        dp->idx++;
    }
    
    int main(void)
    {
        list *l1 = listCreate(sizeof(int));
        for (int i = 0; i < 10; i++)
        {
            //int j = 300 + i;
            //listPushTail(l1, &j);
            listPushTail(l1, &i);
            printf("List forward: ");
            listApplyForward(l1, print_int, stdout);
            putchar('\n');
            printf("List reverse: ");
            listApplyReverse(l1, print_int, stdout);
            putchar('\n');
        }
        int sum = 0;
        listApplyForward(l1, int_sum, &sum);
        printf("Sum = %d\n", sum);
        listDestroy(l1);
    
        list *l2 = listCreate(sizeof(double));
        for (int i = 0; i < 10; i++)
        {
            double d = (rand() % 20000) / 100.0 - 100.0;
            listPushHead(l2, &d);
            printf("List forward: ");
            listApplyForward(l2, print_dbl, stdout);
            putchar('\n');
            printf("List reverse: ");
            listApplyReverse(l2, print_dbl, stdout);
            putchar('\n');
        }
        DblFun d1 = { 0.0, 0 };
        DblFun d2 = { 0.0, 0 };
        listApplyForward(l2, dbl_fun, &d1);
        listApplyReverse(l2, dbl_fun, &d2);
        printf("Fwd: %13.6g, Rev: %13.6g\n", d1.val, d2.val);
        listDestroy(l2);
    
        return 0;
    }
    

    测试输出

    对于双向链表,能够测试正向和反向链接链是很有价值的。并且在构建列表时打印它可以确保它在退化的情况下也有效。

    List forward:  0
    List reverse:  0
    List forward:  0 1
    List reverse:  1 0
    List forward:  0 1 2
    List reverse:  2 1 0
    List forward:  0 1 2 3
    List reverse:  3 2 1 0
    List forward:  0 1 2 3 4
    List reverse:  4 3 2 1 0
    List forward:  0 1 2 3 4 5
    List reverse:  5 4 3 2 1 0
    List forward:  0 1 2 3 4 5 6
    List reverse:  6 5 4 3 2 1 0
    List forward:  0 1 2 3 4 5 6 7
    List reverse:  7 6 5 4 3 2 1 0
    List forward:  0 1 2 3 4 5 6 7 8
    List reverse:  8 7 6 5 4 3 2 1 0
    List forward:  0 1 2 3 4 5 6 7 8 9
    List reverse:  9 8 7 6 5 4 3 2 1 0
    Sum = 45
    List forward:   68.070
    List reverse:   68.070
    List forward:   52.490  68.070
    List reverse:   68.070  52.490
    List forward:    0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730
    List forward:  -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420
    List forward:  -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700
    List forward:   12.720 -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700  12.720
    List forward:  -24.560  12.720 -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700  12.720 -24.560
    List forward:    8.780 -24.560  12.720 -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700  12.720 -24.560   8.780
    List forward:   79.230   8.780 -24.560  12.720 -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700  12.720 -24.560   8.780  79.230
    List forward:   77.090  79.230   8.780 -24.560  12.720 -10.700 -63.420   0.730  52.490  68.070
    List reverse:   68.070  52.490   0.730 -63.420 -10.700  12.720 -24.560   8.780  79.230  77.090
    Fwd:      -27.7014, Rev:    -0.0360992
    

    此代码可在 GitHub 上的 SOQ(堆栈溢出问题)存储库中以文件 genlist.hgenlist.cmain.c 的形式在 src/so-6529-2945 子目录中找到。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-27
      • 2023-03-28
      • 2016-01-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多