有很多方法可以创建一个链表,从简单到令人麻木的 add-at-head(以相反的顺序结束)到一个相当标准的 add-at-tail 遍历节点以找到结束节点,并在那里添加新节点。在所有情况下,只需正确处理指针、分配存储空间(为列表父结构和每个节点)并验证所有分配,然后自行清理并在不再使用时释放已使用的内存需要。
嵌套结构,其中您有一个包含head 节点的结构(希望还有其他有用的数据来证明嵌套方法的合理性)是很常见的,但是列表本身不需要父结构。列表地址就是第一个节点的地址。
在学习列表时,将管理列表的任务分解为简单的单独功能确实很有帮助。这使您可以单独集中(更轻松地)每个列表操作。例如,对于您的列表,您需要:
- 为每个节点创建/分配,初始化
next指针NULL并设置data值;
- 为您的列表创建/分配,为
head 分配并初始化列表结构中包含的任何其他信息;
- 将节点添加到您的列表中,根据需要创建列表并将节点设置数据添加到适当的值并更新所需的任何列表信息;
- 从列表中获取数据;和
- 最终在不再需要时为您的节点和列表释放内存。
在您的情况下,继续我对您问题的评论,您可以声明类似于以下内容的结构和 typedef:
typedef struct node {
int data;
struct node *next;
} node_t;
typedef struct list {
struct node *head;
size_t n; /* at least make parent hold list size */
} list_t;
在这里,我们简单地添加了一个计数器来跟踪列表中的节点数量,作为额外的、有用的数据来证明外部结构的合理性。它为您提供节点数,而无需每次都遍历列表来获取(如果您需要该数据,这只是一个小的效率改进)。您可以通过简单的list->n 获得列表中的节点数。
按照我们的列表大纲,您需要一种方法来为您的列表创建节点。无论是第一个节点,还是最后一个节点,你都不在乎。当您需要一个节点时,您的 create_node 函数应该处理分配/验证和初始化。不需要任何花哨的东西,例如
/** function to create node and set data value to data.
* returns new node on success, NULL otherwise.
*/
node_t *create_node (int data)
{
node_t *newnode = malloc (sizeof *newnode); /* allocate */
if (newnode == NULL) { /* validate/handle error */
perror ("create_node() malloc-newnode");
return NULL;
}
newnode->data = data; /* initialize members */
newnode->next = NULL;
return newnode; /* return pointer to new node */
}
您的create_list 函数只需要为list 结构分配(我还让它分配第一个节点并初始化值和指针0/NULL)。你可以让它做任何你喜欢的事情,例如为第一个节点添加另一个传递data 的参数,等等。我只是让它创建列表和第一个节点。
/** function to create list and allocates/initilizes head, set list->n = 0.
* returns new list on success, NULL otherwise.
*/
list_t *create_list (void)
{
node_t *head = NULL;
list_t *list = malloc (sizeof *list); /* allocate list */
if (!list) { /* validate/handle error */
perror ("create_list() malloc-list");
return NULL;
}
head = create_node (0); /* create the first node */
if (!head) /* validate/handle error */
return NULL;
list->head = head; /* initialize list values */
list->n = 0;
return list; /* return list */
}
您的add_node 函数可能相当简单,但出于此处的目的,如果列表尚未分配,我们将让add_node 函数创建列表,然后添加节点。这一选择具有重要意义。由于我将处理列表不存在的情况,这意味着列表地址可能会在函数内更改。为了处理这种可能性,我必须将列表的 address-of 作为参数传递(例如 list_t **list 而不是简单的 list_t *list)。通过获得指针的实际地址,我可以更改原始指针指向的位置,并且该更改将在调用函数中可见(而不是更改指针的副本指向的位置,而在调用者中不会看到)。
函数需要处理两种情况(1)“我是第一个节点吗?”和(2)“如果我不是第一个节点,那么迭代到结束并添加到那里”。你可以做类似的事情:
/** add node to list, create list if list NULL, set node->data to data.
* return new node on success, NULL otherwise.
*/
node_t *add_node (list_t **list, int data)
{
node_t *node;
if (!*list) { /* handle list doesn't exist */
*list = create_list();
if (!*list)
return NULL;
node = (*list)->head; /* (..)-> required by operator precedence */
node->data = data;
}
else { /* list already exists */
node = (*list)->head; /* set node to list->head */
/* iterate over nodes to find last and add node at end */
while (node->next)
node = node->next;
node->next = create_node (data); /* allocate next node */
node = node->next; /* change to new node */
}
(*list)->n++; /* increment number of nodes in list */
return node; /* return node */
}
通过这种方式,我可以简单地在main() 中声明指针并初始化它NULL,然后在main() 中简单地调用add_node(&list, x),让列表函数处理指针和分配。
您的附加列表函数只是对列表中的每个节点进行迭代的函数,这些函数对信息执行一些操作,例如打印列表或释放列表中的所有节点。 (请注意在free_list 函数中如何处理待释放节点(例如victim)
/** print the value of each node in list */
void prn_list (const list_t *list)
{
/* iterate over list printing data value */
for (node_t *node = list->head; node; node = node->next)
printf (" %d", node->data);
putchar ('\n'); /* tidy up with newline */
}
/** free all nodes in list and free list */
void free_list (list_t *list)
{
node_t *node = list->head; /* set node to head */
while (node) { /* iterate over each nod */
node_t *victim = node; /* setting victim to free */
node = node->next; /* change to next node */
free (victim); /* free victim */
}
free (list); /* free list */
}
(请注意使用for 或while 循环遍历节点的两个不同示例)
将所有部分放在一起,添加25 节点,并在释放与列表关联的所有内存之前将它们打印出来,您可以执行以下操作:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#if ! defined (_WIN32) && ! defined (_WIN64)
#include <stdlib.h> /* Linux has malloc/free in the stdlib header */
#endif
typedef struct node {
int data;
struct node *next;
} node_t;
typedef struct list {
struct node *head;
size_t n; /* at least make parent hold list size */
} list_t;
/** function to create node and set data value to data.
* returns new node on success, NULL otherwise.
*/
node_t *create_node (int data)
{
node_t *newnode = malloc (sizeof *newnode); /* allocate */
if (newnode == NULL) { /* validate/handle error */
perror ("create_node() malloc-newnode");
return NULL;
}
newnode->data = data; /* initialize members */
newnode->next = NULL;
return newnode; /* return pointer to new node */
}
/** function to create list and allocates/initilizes head, set list->n = 0.
* returns new list on success, NULL otherwise.
*/
list_t *create_list (void)
{
node_t *head = NULL;
list_t *list = malloc (sizeof *list); /* allocate list */
if (!list) { /* validate/handle error */
perror ("create_list() malloc-list");
return NULL;
}
head = create_node (0); /* create the first node */
if (!head) /* validate/handle error */
return NULL;
list->head = head; /* initialize list values */
list->n = 0;
return list; /* return list */
}
/** add node to list, create list if list NULL, set node->data to data.
* return new node on success, NULL otherwise.
*/
node_t *add_node (list_t **list, int data)
{
node_t *node;
if (!*list) { /* handle list doesn't exist */
*list = create_list();
if (!*list)
return NULL;
node = (*list)->head; /* (..)-> required by operator precedence */
node->data = data;
}
else { /* list already exists */
node = (*list)->head; /* set node to list->head */
/* iterate over nodes to find last and add node at end */
while (node->next)
node = node->next;
node->next = create_node (data); /* allocate next node */
node = node->next; /* change to new node */
}
(*list)->n++; /* increment number of nodes in list */
return node; /* return node */
}
/** print the value of each node in list */
void prn_list (const list_t *list)
{
/* iterate over list printing data value */
for (node_t *node = list->head; node; node = node->next)
printf (" %d", node->data);
putchar ('\n'); /* tidy up with newline */
}
/** free all nodes in list and free list */
void free_list (list_t *list)
{
node_t *node = list->head; /* set node to head */
while (node) { /* iterate over each nod */
node_t *victim = node; /* setting victim to free */
node = node->next; /* change to next node */
free (victim); /* free victim */
}
free (list); /* free list */
}
int main (void)
{
list_t *list = NULL; /* just declare list and set pointer NULL */
for (int i = 0; i < 25; i++) /* add 25 nodes to list */
if (add_node (&list, i + 1) == NULL) /* validate each addition */
break;
/* print list content, beginning with number of nodes in list */
printf ("list contains: %lu nodes\n\n", list->n);
prn_list (list); /* followed by each node value */
free_list (list); /* and then delete list */
return 0;
}
使用/输出示例
$ /bin/llsingle_w_parent
list contains: 25 nodes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
内存使用/错误检查
在您编写的任何动态分配内存的代码中,对于分配的任何内存块,您都有 2 个职责:(1)始终保留指向起始地址的指针内存块,因此,(2) 当不再需要它时可以释放。
您必须使用内存错误检查程序来确保您不会尝试访问内存或写入超出/超出分配块的边界,尝试读取或基于未初始化的值进行条件跳转,最后,以确认您释放了已分配的所有内存。
对于 Linux,valgrind 是正常的选择。每个平台都有类似的内存检查器。它们都易于使用,只需通过它运行您的程序即可。
$ valgrind ./bin/llsingle_w_parent
==14749== Memcheck, a memory error detector
==14749== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==14749== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==14749== Command: ./bin/llsingle_w_parent
==14749==
list contains: 25 nodes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
==14749==
==14749== HEAP SUMMARY:
==14749== in use at exit: 0 bytes in 0 blocks
==14749== total heap usage: 26 allocs, 26 frees, 416 bytes allocated
==14749==
==14749== All heap blocks were freed -- no leaks are possible
==14749==
==14749== For counts of detected and suppressed errors, rerun with: -v
==14749== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
始终确认您已释放已分配的所有内存并且没有内存错误。
链表有各种不同的实现方式。你应该知道几个基本的区别。您有使用虚拟节点的列表(例如,虚拟 head 和 tail 节点不包含 data,但仅指向列表中的第一个/最后一个节点)。您有 circular-linked-lists,其中最后一个节点指向第一个节点(这允许从列表中的任何节点迭代到 circular中列表中的任何节点> 时尚贯穿起点和终点)。因此,当您查看“链表”代码时,要了解实现列表的方式有很多种,各有优缺点,因此您只需匹配您的列表即可。
最后,正如评论中所指定的,您的声明 void main() 是不正确的,并且是对 Windows 早期时代(如 DOS 3.3 和 Windows 3.1、Trumpet WinSock 和 Borland Turbo C 编译器时代)的古老回归。 main 的声明是 int main (void) 和 int main (int argc, char **argv)(你会看到用等效的 char *argv[] 编写)。 注意: main 是type int 的函数,它返回一个值。请参阅:C11 Standard §5.1.2.2.1 Program startup p1 (draft n1570)。另见:See What should main() return in C and C++?
(注意:有一些嵌入式系统继续使用void main(),但这些是例外情况,而不是规则,并且不符合 C 标准)
查看一下,如果您还有其他问题,请告诉我。