【问题标题】:'Multipurpose' linked list implementation in pure C纯 C 中的“多用途”链表实现
【发布时间】:2010-10-18 16:32:54
【问题描述】:

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

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

我将重点放在本主题中的列表上,因为它通常是我最终在程序中经常使用的一种结构,既可以作为“主”结构,也可以作为其他较大结构的“辅助”结构(例如,哈希使用链表解决冲突的树)。

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

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

为了明确问题:以上哪一项最好?

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

【问题讨论】:

  • 为什么空指针很难调试?这在任何类型的调试器中都是微不足道的,具有良好的表达式评估。
  • 确实如此,而你现在正是我开始思考的时候。我以前的算法教授有点强迫这种抵抗对我们无效的指针。但这仍然使代码难以通过阅读来理解。
  • 嗯,你应该避免使用 void*,只要语言提供了这样的功能......但是如果你想要 c 中的多态......

标签: c linked-list containers generic-programming


【解决方案1】:

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;

【讨论】:

  • 不知道为什么这被三叉戟低估了,一定会喜欢这样的时代。这实际上是最好的答案,添加宏以使分配/访问数据完全通用(例如 tPerson* person = LIST_GET_DATA(pNode, tPerson)
  • 嗯。这是一个非常非常有趣的想法。但我看到了一个问题:似乎我必须在分配时知道有效负载类型的大小。
  • 我想不出任何方法来编写一个“插入列表”方法来完成(a)所有类型的工作和(b)知道添加类型的大小。如果有办法,我很想知道 =)
  • @Rafael,你必须在某个时候知道,分配时间是你能做到的绝对最晚的时间。所有其他解决方案都会有这个“问题”,除非您指定最大大小(仍然必须知道)并且这种方法会导致内存浪费。
  • 顺便说一句,我(显然 - 低代表)没有投票反对你的解决方案,我永远不会。事实上,我喜欢它!尽管我还不确定我是否会使用它(我们的作业记录在案,我必须解释这一切),但我会记住这种方法以备将来使用。非常感谢!
【解决方案2】:

我的 $.002:

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

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

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

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

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

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

  • 你的想法/解决方案

我会选择第一个。

【讨论】:

  • 是的,我也有点抗拒使用'union'方法,但实际上编译时依赖并不是一个大问题,因为一旦分配完成,代码就不会必须被维护。
  • 此外,预处理器“技巧”只是使用带有参数的宏,即列表定义和操作的整个代码。在编译时,预处理器会复制代码,并在适当的地方更改类型。
  • PS:SGLIB 的作者似乎在泛型编程方面具有一定的学术权威,他说 C++ 模板是通过预处理实现的,这就是他的想法。感谢您的贡献!
  • “空指针”方法是我多年前编写的双向链表系统所使用的方法(主要是 1988-9,更新于 2002 年、2008 年)。对于与我相关的 3 种类型,有一组互补的覆盖函数。所以,应用程序使用了特定类型的覆盖函数,...继续...
  • ...继续...但在下面它们都使用了相同的处理代码。这部分是必要的,因为覆盖函数还提供了跨语言粘合层。但这种方法意味着我只需要调试一次“char 指针”(在 1988 年,空指针并不总是可用)版本。
【解决方案3】:

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

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

【讨论】:

  • 感谢您的评论!有人在生产代码中使用这种方法这一事实本身就是一个非常强烈的“赞成无效”论点。
  • 当我不得不这样做时,我对自己完成的实现非常满意。它也运行了好几年。我的主要决定因素是我可以在 100 行代码中使用 void* 来做到这一点——而且很明显可以遵循。其他一切似乎都太复杂/无法维护。
  • 一定有人真的讨厌 void* :P 一定要喜欢 neg。投票!
  • @Reed:是的,坦率地说,我不明白要否决一个完全符合我在最后第二段中要求的答案的意义——这是一个工业界如何完成的例子。
  • 赞成取消一个站不住脚的倒下。多态方法是一种记账麻烦,但它有效且非常灵活。
【解决方案4】:

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

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

【讨论】:

  • 从长远来看,像 linux 内核版本这样的“结构列表”(而不是大多数教授的“结构列表”)是正确的解决方案。没有其他解决方案提供相同级别的通用性。
  • 仅供参考,独立链接现已损坏。
  • list.h 标头链接已失效。
【解决方案5】:

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

【讨论】:

  • 一个很好的提示。实际上我不会在这个正确的项目上使用,只是因为我没有机会在截止日期前亲自与教授交谈。但是,它似乎是一个非常有用的库,我会保留它的书签以供参考。投票!
【解决方案6】:

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

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

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

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

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

【讨论】:

  • 虽然使用更具表现力的语言的建议非常好,但这项作业会教那些其他语言在幕后所做的事情。这就是重点。
  • @dmckee 我同意。我不认为我的回答建议那样做。前两段倒是有点啰嗦“你可以用任何语言写 FORTRAN”。
  • 嗯...是的。我收回。对不起。
  • @tokenmacguy:我完全同意你的观点,“模仿”另一种语言的方面根本不是一个好方法。但是,请注意,我在谈到 SGLIB 时只提到了 C++,而且只是因为它的作者提到了它。
  • @dmckee:实际上,我想澄清一下,分配的目的不是“实现多用途链表”。它实际上是一个有机搜索查询处理器,必须有效地指示经常一起出现的词组。
【解决方案7】:

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

  • 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 * 并完成它。

【讨论】:

  • 非常感谢您提供的信息。一本书的实现当然是一个可引用的、可验证的论点。另外,使用 awk 来创建特定的函数是一种有趣的方法,我没有想到主要是因为我不熟悉繁重的数据处理。
  • 实际上,我有点抗拒使用这种方法(无论使用 cpp 还是 awk 或其他),因为它会产生一些命名麻烦,因为 C 没有命名空间。您如何处理这个问题?
  • 我处理命名空间的方式与 Hanson 相同:通过编程约定。在这种情况下,每个列表函数都包含元素类型的名称或该名称的缩写。我添加了一个示例。
  • “awk 脚本是 150 行的恐怖” - 大多数 150 行的 awk 脚本是:-)
【解决方案8】:

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

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

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

【讨论】:

  • 是的,将 void 放在结构中的想法很好,实际上“软化”了使用 void* 的“粗糙度”。嵌入解释器只是为了使容器结构更通用,这对我来说听起来有点矫枉过正。 (续...)
  • 另外,我不知道使用你没有创建的代码是否有限制:这甚至适用于库,比如说一个完整的解释器。无论如何,我们建议尽可能简单,使用外部解释器根本不成比例。
  • 是的,我只是想发挥创意。 :)
【解决方案9】:

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

【讨论】:

  • 这是一个有效的想法,但我认为它不适合,因为我将使用某些列出的类型存储的数据可能不容易用 XML 表示。无论如何,这对这个问题来说有点过分了。
猜你喜欢
  • 2012-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-13
相关资源
最近更新 更多