在不知道结构实现的情况下如何编写通用列表?
Posted
技术标签:
【中文标题】在不知道结构实现的情况下如何编写通用列表?【英文标题】: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
的通用程度以及您是否想以老式方式(空指针)或现代方式(某种方式的预处理器处理所有支持类型)。此处不同方式的类似用例示例:***.com/a/69379395/584518
为什么不 list_insert (sizeof employee_t, Kevin)
传递要分配的存储大小和指向该存储的指针,以及 malloc()
、memcpy()
例如Kevin
分配给新块并将新块分配给列表的void *
指针数据成员?您仍然需要编写有关如何将 printf
、remove
、query
与 employee_t
类型相关的函数,但您的列表会起作用。这是 C 和类型的 Catch-22。虽然您可以创建一个通用列表并将 function-pointer 作为参数传递给列表操作,但您也可以只为employee_t
编写一个列表。
C 中的主要问题是制作数据硬拷贝的 ADT 不是很明智,因为您要么必须提供 void*
以及大小和/或某些类型标志枚举。或者你必须提供一些带有函数指针的回调接口。因此,实现明智的 ADT 的一种方法是简单地不制作任何数据的硬拷贝,而是将其留给调用者。无论如何,像链表这样带有碎片堆分配的方法已经过时了,因为它在 任何 8 位到 64 位的主流计算机上都不是很有效。
【参考方案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_empty
、list_first
、list_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
会调用一个微妙的未定义行为。
它与在结构成员对象之外但仍在父对象内执行指针算术有关。
请注意,任何对象都可以视为char
s 的数组。
此代码适用于所有主要编译器,并且不太可能失败,因为它会破坏大量现有且大量使用的代码(如 Linux 内核或 Git)。
如果struct node
是嵌入结构的第一个成员,则此程序严格符合,因为 C 标准允许在任何结构与其第一个成员之间进行安全转换。
如果node
不是第一个成员,则严格遵守,
这个问题可以通过形成指向struct list_node
的指针而不是&bob.node
而是在指向bob
的指针上使用指针算法来规避。结果是:
(struct list_node*)((char*)&bob + offsetof(employee_t, node))
但是,这种语法真的很讨厌,所以我个人会选择&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
与节点“指向”的元素共享所有权。以上是关于在不知道结构实现的情况下如何编写通用列表?的主要内容,如果未能解决你的问题,请参考以下文章
是否可以在不使用反射的情况下用 Java 编写一个简单的通用 DAO?
如何在不一遍又一遍地编写相同代码的情况下创建具有动态内容的小部件的长列表? [关闭]