假设我有一个使用以下listnode_t
的列表实现:
typedef struct ListNode {
struct ListNode *next;
struct ListNode *prev;
void *value;
} listnode_t;
这是一个双向链表,如您所见。我还有一个list_t
,它有两个指向listnode_t
的指针作为列表的第一个和最后一个节点和大小。
现在我在主要的
int main(int argc, char *argv[]){
...
// Create two empty lists
list_t *list1 = make_list();
list_t *list2 = make_list();
// Populate one with ints
int x = 4;
int y = 5;
list_push(list1, &x);
list_push(list1, &y);
// Populate other with strings
string_t *str1 = make_string("foo");
string_t *str2 = make_string("bar");
list_push(list2, str1);
list_push(list2, str2);
...
// Delete at the end
destroy_list(list1);
destroy_list(list2);
}
我在实施destroy_list
时遇到问题。这是我试过的;
void destroy_list(list_t *list)
{
listnode_t *cur = list -> first;
for(cur = list -> first; cur != NULL; cur = cur -> next){
if(cur -> prev){
free(cur -> prev);
}
}
free(list -> last);
free(list);
list = NULL;
}
我的问题是,我尝试在void *
中使用listnode_t
来一般使用此列表。但是当我删除内容时,应用free(cur -> prev)
似乎有问题。当我使用像上面这样的内容列表时,因为那些是在堆栈上分配的,所以事情进展顺利,我想。但是,如果我有上面的字符串列表,因为我在字符串实现中使用动态分配,我必须首先应用free(cur -> prev -> value)
。我不知道怎么能这样做,因为如果我添加它,那么我会遇到另一个问题,试图释放主机上堆栈分配的内存。
我该怎么做,我没有得到这种通用列表行为。
答案 0 :(得分:4)
// Populate one with ints
int x, y = 4, 5;
list_push(list1, &x);
list_push(list1, &y);
不要这样做。
您为list
结构建立的接口有效地表示当调用list_push
时指针的“所有权”传递给列表,并且指针将为free()
d在destroy_list
的列表中。将指针传递给堆栈变量会违反该接口 - 如果要传入整数,则需要为列表创建副本以获取所有权。
list
结构的替代接口可能是将指针和长度传递给list_push
,并使该函数获取结构的副本而不是保留它。这是否合适将取决于您对此结构的用例。
(另外,int x, y = 4, 5
没有按照您的想法执行。您可能意味着int x = 4, y = 5
。)
答案 1 :(得分:1)
int x,y = 4,5;
我认为这不符合你的想法。
但是当我删除内容时,应用
cur -> prev
似乎有问题。
“有问题”不是一个技术术语。准确描述您期望发生的事情,以及恰恰发生的事情。
当我使用像上面这样的内容列表时,因为那些是在堆栈上分配的,所以事情进展顺利,我想。
不,他们不是很好。当你在堆栈中获取变量的地址时,你会让自己失望,因为只要你的函数返回,堆栈就是垃圾。
但如果我有上面的字符串列表,因为我在字符串实现中使用动态分配,我必须首先应用
cur -> prev -> value
“申请”是什么意思?你是什么意思“第一”?
我不知道怎么能这样做,因为如果我添加它,
“添加”是什么意思?它是什么”?添加到什么?在技术文本中,我们从不使用“it”和“that”这样的介词。拼写出来。
然后我又遇到了另一个试图释放主机上堆栈分配内存的问题。
同样,“问题”并没有多说。在任何情况下,如果我猜测您的问题是什么,解决方案很简单:始终为value
分配空间,以便您可以随时释放它。
if(cur -> prev){ free(cur -> prev); }
这不是你从双向链表中删除节点的方式。为了从双向链表中删除节点Y,您必须首先取消链接节点Y,以便节点Z的prev
指向节点X,并且节点X的next
指向节点Z,然后你可以释放节点Y.(释放它的值之后。)当然你必须时刻记住,节点Y可能是列表中的第一个或最后一个节点,在这种情况下它没有上一个或下一个节点,而你必须修改列表结构本身的头部或尾部。
答案 2 :(得分:1)
用户必须提供销毁value
的功能,因为只有列表的用户知道存储的内容和需要执行的操作。
为了使您的列表成为通用列表,make_list
必须将作为参数的参数作为销毁值的函数。作为一个部分例子:
struct LISTROOT {
listnode_t list;
void (*freeitem)(void * value);
};
struct LISTROOT myList;
struct MYTYPE {
char *buffer;
int *whatever;
};
void myFreeItem(struct MYTYPE *item)
{
if (item) {
if (item->buffer) free(item->buffer);
...
free(item);
}
}
myList.list= make_list(myFreeItem);
并在您的代码中:
if(cur -> prev){
if (myList.freeitem) mylist.freeitem(cur->value);
free(cur -> prev);
请注意,列表定义freeitem
以获取void *
值,而用户的myFreeItem
则指向更复杂的struct
。编译器接受此操作并允许您隐藏列表中存储的任何内容的复杂性。
答案 3 :(得分:0)
一个非常常见的模型是接口的调用者指定是否应该由容器释放内容。即
list_t *list1 = make_list(0); // dont free items
list_t *list1 = make_list(1); // free items
如果他们说'请免费',那么您只需致电free
即可。通过调用者必须提供的通用“这里是一个免费回调”更简单(另一个答案建议)
答案 4 :(得分:0)
一个简单且相当安全的解决方案是让列表管理传入的值的副本。因此,列表负责正确分配和释放内存。
请参阅以下代码,以相应地调整struct ListNode
和list_push
- 函数。请注意,该结构不再包含指向该值的指针;它将值存储在变长成员value[]
中,并为结构本身分配足够的内存。因此,稍后释放struct对象也会自动释放内存以获取值副本。
typedef struct ListNode {
struct ListNode *next;
struct ListNode *prev;
char value[];
} listnode_t;
void list_push(list_t *t, void *value, size_t valueSize) {
listnode_t *newNode = malloc(sizeof(listnode_t) + valueSize);
memcpy (newNode->value, value, valueSize);
// ... any code necessary for attaching the new node to the list goes here
}