【问题标题】:How do you write generic list without knowing the implementation of structure?在不知道结构实现的情况下如何编写通用列表?
【发布时间】:2021-10-22 10:28:36
【问题描述】:

假设有一个员工ADT,比如

//employee.h

typedef struct employee_t employee_t;

employee_t* employee_create(char* company, char* department, char* position);
void employee_free(employee_t* me);

,客户端代码是

#include "employee.h"

employee_t* Kevin = employee_create("Facebook", "Marketing", "Sales");
employee_t* John = employee_create("Microsoft", "R&D", "Engineer");

现在客户想使用列表 ADT 来插入 Kevin 和 John 来列出某些任务。

//list.h

typedef struct list_t list_t;

list_t* list_create(/*might have some arguments*/);

所以客户端代码将是

#include "employee.h"
#include "list.h"

employee_t* Kevin = employee_create("Facebook", "Marketing", "Sales");
employee_t* John = employee_create("Microsoft", "R&D", "Engineer");

list_t* employee = list_create(/*might have some arguments*/);
list_insert(employee, Kevin);
list_insert(employee, John);

employee_free(Kevin);
employee_free(John);

list_print(employee); //Oops! How to print structure that you can't see?

由于employee被不透明指针封装,list无法复制。

list的ADT怎么写和实现?

【问题讨论】:

  • 为什么不将void* 存储在您的列表中?
  • 您的问题不清楚:“如何编写此列表 ADT 和实现?” 阻碍您编写此列表 ADT 和实现的问题是什么?
  • 这种类型取决于list_t 的通用程度以及您是否想以老式方式(空指针)或现代方式(某种方式的预处理器处理所有支持类型)。此处不同方式的类似用例示例:stackoverflow.com/a/69379395/584518
  • 为什么不 list_insert (sizeof employee_t, Kevin) 传递要分配的存储大小和指向该存储的指针,以及 malloc()memcpy() 例如Kevin 分配给新块并将新块分配给列表的void * 指针数据成员?您仍然需要编写有关如何将 printfremovequeryemployee_t 类型相关的函数,但您的列表会起作用。这是 C 和类型的 Catch-22。虽然您可以创建一个通用列表并将 function-pointer 作为参数传递给列表操作,但您也可以只为employee_t 编写一个列表。
  • C 中的主要问题是制作数据硬拷贝的 ADT 不是很明智,因为您要么必须提供 void* 以及大小和/或某些类型标志枚举。或者你必须提供一些带有函数指针的回调接口。因此,实现明智的 ADT 的一种方法是简单地不制作任何数据的硬拷贝,而是将其留给调用者。无论如何,像链表这样带有碎片堆分配的方法已经过时了,因为它在 任何 8 位到 64 位的主流计算机上都不是很有效。

标签: c encapsulation generic-list abstract-data-type opaque-pointers


【解决方案1】:

执行此操作的常用方法是让您的列表结构将数据存储为void*。例如,假设你的列表是一个单链表:

struct list_t
{
  void *data;
  struct list_t *next;
};

现在list_insert 应该是这样的:

list_t *list_insert(list_t *head, void *data)
{
  list_t *newHead = (list_t*)malloc(sizeof(list_t));
  newHead->data;
  newHead->next = head;

  return newHead;
}

如果你想隐藏结构的实现,那么你可以添加方法来提取数据。例如:

void *list_get_data(list_t *head)
{
  return head->data;
}

【讨论】:

    【解决方案2】:

    不知道结构实现的情况下如何写泛型列表?

    创建抽象处理结构的函数。

    list的ADT怎么写和实现?

    list_create(); 需要传入特定对象类型的辅助函数指针以执行各种任务抽象地

    • void *employee_copy(const void *emp) 这样的copy 函数,所以list_insert(employee, Kevin); 知道如何复制Kevin

    • void employee_free(void *emp) 这样的free 函数,所以list_uninsert(employee_t) 可以在列表被销毁或成员被一一删除时释放。

    • print 函数 int employee_print(void *emp) 所以 list_print(employee_t) 知道如何打印其列表中的每个成员。

    • 可能还有其他人。

    与其传入3+个函数指针,不如考虑传入一个包含这些指针的struct,那么列表只需要1个指针的开销:list_create(employee_t_function_list)

    你正在朝着重写 C++ 迈出第一步

    【讨论】:

    • 您也可以为特定类型生成这些函数,例如list_create_employee。虽然这涉及到很多邪恶的 X 宏......
    • 关于剧透:不要太早! :)
    【解决方案3】:

    你可以使用一种叫做侵入式列表的东西。这个概念在 Linux 内核中被大量使用。 您只需将节点嵌入到结构中,并让通用代码仅在此结构成员上运行。

    #include <stddef.h>
    
    struct list_node {
      struct list_node *next;
    };
    
    struct list_head {
      struct list_node *first;
    };
    
    /* translates pointer to a node to pointer to containing structure
     * for each pointer `ptr` to a `struct S` that contain `struct list_node node` member:
     *   list_entry(&ptr->node, S, node) == ptr
     */
    #define list_entry(ptr, type, member) \
      (type*)((char*)ptr - offsetof(type, member))
    
    void list_insert(struct list_head *head, struct list_node *node) {
      node->next = head->first;
      head->first = node;
    }
    
    #define LIST_FOREACH(it, head) \
      for (struct list_node *it = (head)->first; it; it = it->next)
    
    

    接口可以很容易地被list_is_emptylist_firstlist_remove_first等其他助手扩展,将大小嵌入到struct list_head

    示例用法:

    typedef struct {
      char *name;
      struct list_node node;
    } employee_t;
    
    typedef struct {
      char *name;
      struct list_head employees;
    } employer_t;
    
    employer_t company = { .name = "The Company" };
    employee_t bob = { .name = "Bob" };
    employee_t mark = { .name = "Mark" };
    
    list_insert(&company.employees, &bob.node);
    list_insert(&company.employees, &mark.node);
    
    printf("Employees of %s:\n", company.name);
    LIST_FOREACH(n, &company.employees) {
      employee_t *e = list_entry(n, employee_t, node);
      printf("%s\n", e->name);
    }
    

    打印:

    Employees of The Company:
    Mark
    Bob
    

    请注意,list_* 接口也可以轻松用于其他类型。

    请参阅article,了解有关将此概念用于双链表的更多信息。


    编辑

    请注意,list_entry 会调用一个微妙的未定义行为。 它与在结构成员对象之外但仍在父对象内执行指针算术有关。 请注意,任何对象都可以视为chars 的数组。

    此代码适用于所有主要编译器,并且不太可能失败,因为它会破坏大量现有且大量使用的代码(如 Linux 内核或 Git)。

    如果struct node 是嵌入结构的第一个成员,则此程序严格符合,因为 C 标准允许在任何结构与其第一个成员之间进行安全转换。

    如果node 不是第一个成员,则严格遵守, 这个问题可以通过形成指向struct list_node 的指针而不是&amp;bob.node 而是在指向bob 的指针上使用指针算法来规避。结果是:

    (struct list_node*)((char*)&bob + offsetof(employee_t, node))
    

    但是,这种语法真的很讨厌,所以我个人会选择&amp;bob.node

    【讨论】:

    • 另外,LIST_FOREACH 编辑使代码变得更糟。不要用神秘的秘密宏重新发明 C 语言。
    • @Lundin,只有“算术越界”在技术上是正确的,这个宏是 container_of 宏的变体,广泛用于 C 项目。未对齐和违反严格的别名规则不适用于此处,因为指向 list_node 的指针是从正确创建嵌入结构中构造的
    • @Lundin,这里没有重新发明,这些概念在大多数流行的 C 项目中大量使用,包括 Git:github.com/git/git/blob/master/list.h,Linux 内核:github.com/torvalds/linux/blob/master/include/linux/list.h
    • 嗯,好吧,我并没有真正遵循代码(这反过来意味着它很难阅读)。无论如何,那个宏是非常危险的。但是对于蹩脚代码库的热爱,请不要再将 Linux 称为编写良好且可移植的东西。
    • @Lundin,它不难阅读,它仍然很简单,尽管在概念上与将void* 放入列表节点不同。这种解决方案在访问节点时有利于提高 less 间接性。这意味着更少的缓存未命中和更好的性能。此外,它还简化了内存管理,因为node 与节点“指向”的元素共享所有权。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多