纯C中的“多用途”链表实现

时间:2009-04-10 00:08:23

标签: c linked-list containers generic-programming

这不完全是一个技术问题,因为我知道C足以做我需要做的事情(我的意思是,不是'让语言妨碍你'),所以这个问题基本上是一个问题。 '采取什么方向'问题。

情况是:我目前正在学习高级算法课程,为了“成长为程序员”,我需要使用纯C来实现实际作业(它运作良好:几乎任何小错误你make实际上迫使你完全理解你正在做的事情以便修复它。在实现过程中,我显然遇到了必须从头开始实现“基本”数据结构的问题:实际上不仅是链表,还有堆栈,树等等。

我专注于本主题中的列表,因为它通常是一个结构,我最终在程序中使用了很多,作为“主”结构或作为其他更大结构的“帮助”结构(例如,哈希)通过链接列表解决冲突的树。)

这要求列表存储许多不同类型的元素。我假设这里作为一个前提,我不想为每种类型重新编码列表。所以,我可以提出这些替代方案:

  • 制作一个无效指针列表(有点不优雅;难以调试)
  • 只有一个列表,但是 union 为'element type',包含我将在程序中使用的所有元素类型(更容易调试;如果元素的大小不同,则会浪费空间)
  • 使用预处理器宏为SGLIB的样式重新生成每种类型的代码,'模仿'C ++的STL(创造性的解决方案;不浪费空间;元素具有它们实际上的显式类型)返回; 列表代码中的任何更改都可能非常引人注目
  • 您的想法/解决方案

要明确问题:上述哪一个最好?

PS:因为我基本上处于学术背景中,所以我对在业内使用纯C的人的观点也非常感兴趣。我知道大多数纯C程序员都在嵌入式设备领域,我不认为我面临的这种问题很常见。但是,如果那里的任何人知道它在现实世界中是如何完成的,我会对你的意见非常感兴趣。

9 个答案:

答案 0 :(得分:33)

void *在链表中有点痛苦,因为你必须将它的分配单独管理到列表本身。我过去使用的一种方法是使用“可变大小”结构,如:

typedef struct _tNode {
    struct _tNode *prev;
    struct _tNode *next;
    int payloadType;
    char payload[1];  // or use different type for alignment.
} tNode;

现在我意识到看起来变量大小但是让我们分配一个结构:

typedef struct {
    char Name[30];
    char Addr[50];
} tPerson;
tNode *node = malloc (sizeof (tNode) - 1 + sizeof (tPerson));

现在你有一个节点,出于所有意图和目的,它看起来像这样:

typedef struct _tNode {
    struct _tNode *prev;
    struct _tNode *next;
    int payloadType;
    char Name[30];
    char Addr[50];
} tNode;

或以图形形式(其中[n]表示n字节):

+----------------+
|    prev[4]     |
+----------------+
|    next[4]     |
+----------------+
| payloadType[4] |                
+----------------+                +----------+
|   payload[1]   | <- overlap ->  | Name[30] |
+----------------+                +----------+
                                  | Addr[50] |
                                  +----------+

也就是说,假设您知道如何正确地处理有效负载。这可以按如下方式完成:

node->prev = NULL;
node->next = NULL;
node->payloadType = PLTYP_PERSON;
tPerson *person = &(node->payload); // cast for easy changes to payload.
strcpy (person->Name, "Bob Smith");
strcpy (person->Addr, "7 Station St");

该转换行只是将payload字符的地址(在tNode类型中)转换为实际tPerson有效内容类型的地址。

使用此方法,您可以在节点中携带所需的任何有效负载类型,甚至每个节点中的不同的有效负载类型,而不会浪费联合的空间。可以通过以下方式看到这种浪费:

union {
    int x;
    char y[100];
} u;

每次在列表中存储整数类型时浪费96个字节(对于4字节整数)。

tNode中的有效负载类型允许您轻松检测此节点承载的有效负载类型,因此您的代码可以决定如何处理它。您可以使用以下内容:

#define PAYLOAD_UNKNOWN     0
#define PAYLOAD_MANAGER     1
#define PAYLOAD_EMPLOYEE    2
#define PAYLOAD_CONTRACTOR  3

或(可能更好):

typedef enum {
    PAYLOAD_UNKNOWN,
    PAYLOAD_MANAGER,
    PAYLOAD_EMPLOYEE,
    PAYLOAD_CONTRACTOR
} tPayLoad;

答案 1 :(得分:8)

我的$ .002:

  • 列出一个无效指针列表(有点不好;难以调试)

这不是一个糟糕的选择,恕我直言,如果必须用C语言编写。您可以添加API方法以允许应用程序提供print()方法以便于调试。当(例如)项目被添加到列表或从列表中删除时,可以调用类似的方法。 (对于链表,这通常不是必需的,但对于更复杂的数据结构 - 例如哈希表) - 它有时可以成为救星。)

  • 只创建一个列表,但将联合作为'元素类型',包含我将在程序中使用的所有元素类型(更容易调试;如果元素的大小不同,则浪费空间)

我会像瘟疫一样避免这种情况。 (好吧,你确实问过。)从数据结构到其包含的类型具有手动配置的编译时依赖性是所有世界中最糟糕的。再次,恕我直言。

  • 使用预处理器宏为SGLIB(sglib.sourceforge.net)的样式重新生成每种类型的代码,“模仿”C ++的STL(创造性解决方案;不浪费空间;元素实际上具有明确的类型是什么时候返回;列表代码的任何变化都可能非常引人注目)

有趣的想法,但由于我不知道SGLIB,我不能说更多。

  • 您的想法/解决方案

我选择第一个选择。

答案 2 :(得分:6)

我在过去,在我们的代码中已经完成了这个(之后已经转换为C ++),并且当时决定使用void *方法。我只是为了灵活性而这样做 - 我们几乎总是在列表中存储一个指针,解决方案的简单性和它的可用性(对我而言)超过了其他方法的缺点。

话虽这么说,有一次它引起了一些难以调试的讨厌的bug,所以它绝对不是一个完美的解决方案。但是,如果我现在再次这样做,我认为它仍然是我要采取的那个。

答案 3 :(得分:6)

使用预处理器宏是最佳选择。 Linux kernel linked list是C中循环链接列表的优秀实现。非常便携且易于使用。 Here Linux内核2.6.29 list.h头文件的独立版本。

FreeBSD / OpenBSD sys/queue是基于通用宏的链表的另一个不错的选择

答案 4 :(得分:4)

我几年没有编码C,但是GLib声称提供了“字符串和公共数据结构的大量实用函数”,其中包括链接列表。

答案 5 :(得分:1)

这是一个很好的问题。我喜欢两种解决方案:

  • Dave Hanson的C Interfaces and Implementations使用void *指针列表,这对我来说已经足够了。

  • 对于我的学生,我写了一个 awk脚本来生成特定于类型的列表函数。与预处理器宏相比,它需要额外的构建步骤,但是对于没有大量经验的程序员来说,系统的操作更加透明。它确实有助于形成参数多态,他们将在后来的课程中看到它们。

    以下是一组函数的外观:

    int      lengthEL (Explist *l);
    Exp*     nthEL    (Explist *l, unsigned n);
    Explist *mkEL     (Exp *hd, Explist *tl);
    

    awk脚本是一个150行的恐怖片;它在C代码中搜索typedef并为每个代码生成一组列表函数。它太老了;我现在可能做得更好了: - )

我不会在一天中的时间(或我硬盘上的空间)给出工会列表。它不安全,而且不可扩展,所以你也可以使用void *并完成它。

答案 6 :(得分:1)

虽然考虑使用另一种语言的技术来解决这类问题很有诱惑力,比如,仿制药,但在实践中它很少会成功。可能有一些固定解决方案可以在大多数情况下正确使用(并在他们的文档中告诉你错误的时候),使用它可能会错过任务的重点,所以我会三思而后行。对于极少数情况,推出自己的可能是可行的,但对于任何合理大小的项目,它不太可能值得调试。

相反,当用x语言编程时,你应该使用x语言的习语。在使用python时不要编写java。在使用方案时不要写C.使用C99时不要编写C ++。

我自己,我可能最终会使用类似Pax的建议,但实际上使用char [1]和void *和int的联合,以使常见情况更方便(以及一个enumed类型标志)

(我也可能最终会实现一个斐波纳契树,只是因为听起来很整洁,你只能在失去它的味道之前多次实施RB树,即使这对于普通情况更好。用于。)

根据您的评论

编辑,看起来您使用固定解决方案的情况非常好。如果您的教练允许它,并且它提供的语法感觉舒适,请给它一个旋转。

答案 7 :(得分:0)

使其成为void *列表的一个改进是使它成为包含void *的结构列表和一些关于void *指向的元数据,包括其类型,大小等。

其他想法:嵌入Perl或Lisp解释器。

或者中途:与Perl库链接并将其列为Perl SV或其他内容。

答案 8 :(得分:0)

我可能会自己使用void *方法,但我发现你可以将数据存储为XML。然后列表可以只有一个char *用于数据(您可以根据需要解析所需的任何子元素)....