28

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

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

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

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

  • 制作一个 void 指针列表(有点不优雅;更难调试)
  • 只制作一个列表,但有一个联合作为“元素类型”,包含我将在程序中使用的所有元素类型(更易于调试;如果元素大小不同,则会浪费空间)
  • 使用预处理器宏以SGLIB的样式为每种类型重新生成代码,“模仿”C++ 的 STL(创造性的解决方案;不浪费空间;元素在返回时具有它们实际的显式类型;列表中的任何更改代码真的很戏剧化
  • 你的想法/解决方案

把问题说清楚:以上哪一个是最好的?

PS:由于我基本上是在学术环境中,我也对业内使用纯 C 的人的观点非常感兴趣。我知道大多数纯C程序员都在嵌入式设备领域,我认为我面临的这种问题并不常见。但是,如果有人知道它是如何“在现实世界中”完成的,我会对您的意见非常感兴趣。

4

9 回答 9

33

Avoid *在链表中有点麻烦,因为您必须单独管理它对链表本身的分配。我过去使用的一种方法是具有“可变大小”结构,例如:

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;
于 2009-04-10T00:49:31.667 回答
8

我的 .002 美元:

  • 制作一个 void 指针列表(有点不雅;更难调试)

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

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

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

  • 使用预处理器宏为每种类型重新生成代码,采用 SGLIB (sglib.sourceforge.net) 的样式,“模仿”C++ 的 STL(创造性的解决方案;不浪费空间;元素具有它们实际的显式类型被返回;列表代码中的任何更改都可能非常显着)

有趣的想法,但由于我不了解 SGLIB,所以我不能说更多。

  • 你的想法/解决方案

我会选择第一选择。

于 2009-04-10T00:22:02.857 回答
6

使用预处理器宏是最好的选择。Linux 内核链表是 C 中循环链表的一种出色的高效实现。非常便携且易于使用。这里是 linux 内核 2.6.29 list.h 标头的独立版本。

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

于 2009-06-07T07:53:38.877 回答
6

我过去曾在我们的代码中(后来已转换为 C++)中这样做过,当时我决定采用 void* 方法。我这样做只是为了灵活性——无论如何,我们几乎总是在列表中存储一个指针,解决方案的简单性和可用性超过了(对我而言)其他方法的缺点。

话虽如此,曾经有一次它导致了一些难以调试的讨厌的错误,所以它绝对不是一个完美的解决方案。不过,如果我现在再次这样做,我认为它仍然是我会采取的。

于 2009-04-10T00:24:04.957 回答
4

我已经很多年没有编写过 C 代码了,但GLib声称提供“大量用于字符串和通用数据结构的实用函数”,其中包括链表。

于 2009-04-10T01:05:37.867 回答
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 代码中搜索typedefs 并为每个函数生成一组列表函数。它很旧;我现在可能会做得更好:-)

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

于 2009-04-10T01:22:41.167 回答
1

尽管考虑使用另一种语言的技术(例如泛型)解决此类问题很诱人,但实际上它很少能成功。可能有一些罐头解决方案在大多数情况下都能做到正确(并在他们出错时在他们的文档中告诉你),使用它可能会错过任务的重点,所以我会三思而后行。在极少数情况下,自己动手可能是可行的,但对于任何合理规模的项目,它可能不值得进行调试工作。

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

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

(我也可能最终实现一个斐波那契树,只是因为这听起来很整洁,而且你只能在它失去它的味道之前多次实现 RB 树,即使这对于它用于的常见情况来说更好.)

编辑: 根据您的评论,您似乎有一个很好的案例来使用罐装解决方案。如果你的教练允许,并且它提供的语法感觉很舒服,那就试一试。

于 2009-04-10T01:23:23.223 回答
0

将其设为 void* 列表的一项改进是将其设为包含 void* 和一些有关 void* 指向的元数据的结构列表,包括其类型、大小等。

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

或者半途而废:与 Perl 库链接并使其成为 Perl SV 列表或其他东西。

于 2009-04-10T02:19:55.350 回答
0

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

于 2009-04-10T02:22:43.410 回答