10. 结构体综合实战:链表节点的定义、创建、遍历与销毁
链表这东西,说实话,是C语言里绕不开的一道坎。我当年刚学的时候也觉得挺玄乎,什么节点、指针、动态分配,一堆概念搅在一起。但后来在项目里真正用上了,才发现它其实没那么复杂——说白了,就是一组结构体手拉手排成一串。
今天咱们就把链表这层窗户纸捅破。从节点定义开始,一步步把创建、遍历、销毁全走一遍。你跟着我敲一遍代码,保证能搞明白。
10.1 链表节点长什么样?
链表的基本单位叫「节点」。每个节点存两样东西:一是数据,二是指向下一个节点的指针。用结构体来表达,就是:
// 定义一个单向链表节点
struct Node {
int data; // 数据域
struct Node* next; // 指针域
};
你看,这个结构体里有个struct Node*类型的成员。它指向的是和自己同类型的结构体。这种「自己指自己」的写法,就是链表的精髓。
核心理解:节点就像一节火车车厢。车厢里装着货物(data),车厢后面有个挂钩(next),可以挂上下一节车厢。最后一节车厢的挂钩是空的(NULL),表示到头了。
我个人习惯给节点类型起个别名,省得每次写struct Node那么长:
typedef struct Node {
int data;
struct Node* next;
} Node;
这样后面用Node*就清爽多了。
10.2 创建节点——动态分配内存
链表节点不是静态变量,而是在程序运行时动态申请的。用malloc在堆上分配内存,然后初始化数据。
// 创建一个新节点,返回节点指针
Node* createNode(int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败!\n");
return NULL;
}
newNode->data = value;
newNode->next = NULL;
return newNode;
}
这里有个坑,我曾经踩过——一定要检查malloc的返回值。嵌入式环境里内存本来就紧张,分配失败是常有的事。不检查就直接用,程序崩了都不知道怎么死的。
注意:malloc返回的是void*,需要强制转换成Node*。另外,sizeof(Node)计算的是整个结构体的大小,包括可能的对齐填充字节。
10.3 链表的创建——头插法与尾插法
有了节点,怎么把它们串起来?两种常用方式:头插法和尾插法。
10.3.1 头插法
每次新节点都插在链表头部。新节点的next指向原来的头节点,然后更新头指针。
// 头插法:新节点成为新的头
void insertAtHead(Node** head, int value) {
Node* newNode = createNode(value);
if (newNode == NULL) return;
newNode->next = *head;
*head = newNode;
}
为什么传Node**?因为要修改头指针本身。如果只传Node*,你改的是形参,外面的头指针不会变。这个细节我当年也迷糊过一阵子。
10.3.2 尾插法
新节点加在链表末尾。需要先找到最后一个节点,然后把它的next指向新节点。
// 尾插法:新节点成为新的尾
void insertAtTail(Node** head, int value) {
Node* newNode = createNode(value);
if (newNode == NULL) return;
if (*head == NULL) {
*head = newNode;
return;
}
Node* temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
尾插法保持了数据的插入顺序,但每次都要遍历到末尾,效率比头插法低。你想想看,如果链表有1000个节点,每次插入都要走1000步。所以实际项目中,如果对顺序没要求,我一般用头插法。
10.4 遍历链表——把数据走一遍
遍历就是从头节点开始,沿着next指针一路走下去,直到遇到NULL。
// 遍历链表,打印每个节点的数据
void traverseList(Node* head) {
Node* current = head;
int count = 0;
while (current != NULL) {
printf("节点 %d: %d\n", count++, current->data);
current = current->next;
}
printf("共 %d 个节点\n", count);
}
这里有个小技巧:用current指针来遍历,不要直接移动head。因为head是链表的入口,丢了就找不回来了。我见过有人直接head = head->next,遍历完链表头也没了,后面想再操作就抓瞎了。
小提示:遍历时如果只需要读数据,用const Node*更安全,能防止不小心修改了链表结构。
10.5 销毁链表——别忘了释放内存
链表用完了,必须把每个节点都free掉。否则就会造成内存泄漏。嵌入式系统内存有限,泄漏一点可能就出大问题。
// 销毁整个链表,释放所有节点内存
void destroyList(Node** head) {
Node* current = *head;
Node* nextNode;
while (current != NULL) {
nextNode = current->next; // 先保存下一个节点
free(current); // 释放当前节点
current = nextNode; // 移动到下一个
}
*head = NULL; // 头指针置空,防止野指针
}
注意顺序:先保存下一个节点,再释放当前节点。如果先free(current)再取current->next,那就是访问已释放的内存,行为未定义。我曾经在项目里犯过这个错,排查了好久才发现是释放顺序搞反了。
警告:销毁后一定要把头指针置为NULL。否则这个指针就成了野指针,后面不小心用了它,程序崩溃是轻的,数据被破坏才真要命。
10.6 完整示例——把上面串起来
咱们写个完整的例子,从创建到销毁走一遍:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
Node* createNode(int value) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) return NULL;
newNode->data = value;
newNode->next = NULL;
return newNode;
}
void insertAtHead(Node** head, int value) {
Node* newNode = createNode(value);
if (newNode == NULL) return;
newNode->next = *head;
*head = newNode;
}
void traverseList(Node* head) {
Node* current = head;
int count = 0;
while (current != NULL) {
printf("节点 %d: %d\n", count++, current->data);
current = current->next;
}
}
void destroyList(Node** head) {
Node* current = *head;
Node* nextNode;
while (current != NULL) {
nextNode = current->next;
free(current);
current = nextNode;
}
*head = NULL;
}
int main() {
Node* head = NULL;
// 头插法创建链表:5 -> 10 -> 15 -> 20
insertAtHead(&head, 20);
insertAtHead(&head, 15);
insertAtHead(&head, 10);
insertAtHead(&head, 5);
printf("链表内容:\n");
traverseList(head);
// 销毁链表
destroyList(&head);
printf("链表已销毁,head = %p\n", (void*)head);
return 0;
}
运行结果:
链表内容:
节点 0: 5
节点 1: 10
节点 2: 15
节点 3: 20
链表已销毁,head = (nil)
10.7 链表的核心逻辑图
下面这张图把链表的核心操作串起来了,你看一遍就能记住:
10.8 避坑指南——我踩过的那些坑
链表这东西,代码看着简单,真写起来容易出各种问题。我把自己踩过的坑列出来,你写的时候多留个心眼:
| 常见错误 | 后果 | 正确做法 |
|---|---|---|
忘记检查malloc返回值 |
空指针访问,程序崩溃 | 每次malloc后都判断是否为NULL |
| 遍历时直接移动头指针 | 丢失链表入口 | 用临时指针遍历,不动head |
| 释放节点后继续访问 | 未定义行为,可能崩溃 | 先保存next,再free当前节点 |
销毁后头指针不置NULL |
野指针,后续误用 | *head = NULL |
传参时只传Node*却想修改头指针 |
修改无效 | 传Node**,即指针的指针 |
我曾经在一个项目里,因为忘了把销毁后的头指针置NULL,结果后面代码里又用这个指针去遍历,读出来的全是垃圾数据。排查了整整一个下午才发现问题。嗯,从那以后我每次销毁链表都会顺手把头指针清掉,已经成肌肉记忆了。
个人建议:写链表操作时,养成「先检查、后使用」的习惯。检查指针是否NULL,检查malloc是否成功,检查边界条件。这些检查看起来啰嗦,但能帮你省下大量调试时间。
链表是很多复杂数据结构的基础。栈、队列、图,底层都可以用链表实现。你把今天这几个操作练熟了,后面学什么数据结构都轻松。
公众号:蓝海资料掘金营,微信deep3321