【嵌入式 C 语言实战】链表从入门到精通:存储原理 + 核心操作 + 函数详解
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,数据存储是核心需求之一,而链表作为一种灵活的链式存储结构,完美解决了数组 “地址连续、扩容困难” 的痛点,在串口数据缓存、设备链表管理、动态数据存储等场景中应用广泛。今天就结合资料,从基础概念到实战操作,系统讲解链表的核心知识点,帮你彻底掌握这种嵌入式必备数据结构。
一、链表的核心基础:为什么需要链表?
1. 链表与数组的本质区别
数据存储主要分为两种方式,各有优劣,嵌入式开发中需按需选择:
- 数组(线性存储):地址连续,可随机访问,但扩容需重新分配内存,容易造成内存浪费;
- 链表(链式存储):节点地址不连续,通过指针串联,支持动态扩容,无需预先分配固定内存,内存利用率更高。
2. 链表的组成:节点结构
链表由一个个 “节点” 串联而成,每个节点包含两部分,通过结构体定义:
c
运行
#include <stdio.h>
#include <stdlib.h>
// 链表节点结构体定义
struct Node {
int data; // 数据域:存储实际业务数据(如传感器值、设备ID)
struct Node *pnext;// 指针域:存储下一个节点的地址,实现节点串联
};
- 数据域:可根据需求替换为 int、char 数组、结构体等任意类型;
- 指针域:必须是同类型节点的指针,确保能指向后续节点。
3. 链表的核心支撑函数
链表的创建、删除等操作依赖内存管理函数,这是嵌入式开发中必须掌握的基础:
| 函数名 | 核心功能 | 头文件 | 关键参数与说明 |
|---|---|---|---|
malloc | 手动开辟内存空间 | <stdlib.h> | size_t size:开辟空间大小(如sizeof(struct Node));返回值需强转,失败返回 NULL |
memset | 初始化内存空间 | <string.h> | void *s:空间首地址;int c:初始化内容(常用 0);size_t n:空间大小 |
bzero | 初始化内存空间(置 0) | <strings.h> | void *s:空间首地址;size_t n:空间大小;无返回值,直接置 0 |
free | 释放已开辟的内存空间 | <stdlib.h> | void *ptr:需释放的空间地址;必须调用,避免内存泄漏 |
嵌入式注意点
malloc可能分配失败,必须添加判空逻辑(如if(node == NULL) return NULL;),避免野指针;- 嵌入式设备内存有限,
free释放内存后,建议将指针置为 NULL(如node = NULL),防止二次释放。
二、链表的核心操作:从创建到删除(附实战代码)
本文以带头节点的单链表为例(嵌入式开发最常用),头节点仅用于标识链表起始位置,不存储有效数据,能简化边界处理。
1. 第一步:创建节点(链表的基础单元)
创建节点是所有链表操作的前提,流程为 “定义类型→开辟空间→初始化→返回地址”:
c
运行
// 创建单个链表节点
struct Node *createNode(int data) {
// 1. 开辟节点内存空间
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
if (newNode == NULL) {
printf("malloc error: 节点内存分配失败\n");
return NULL;
}
// 2. 初始化节点(两种方式任选)
memset(newNode, 0, sizeof(struct Node)); // 用memset置0
// bzero(newNode, sizeof(struct Node)); // 用bzero置0(效果相同)
// 3. 填充数据域
newNode->data = data;
newNode->pnext = NULL; // 新节点初始无后续节点,指针置NULL
// 4. 返回新节点地址
return newNode;
}
2. 第二步:初始化链表(创建头节点)
初始化链表的核心是创建头节点,标识链表的起始位置:
c
运行
// 初始化链表(返回头节点)
struct Node *initLinkList() {
// 创建头节点(数据域无意义,仅用指针域)
struct Node *phead = (struct Node *)malloc(sizeof(struct Node));
if (phead == NULL) {
printf("malloc error: 头节点内存分配失败\n");
exit(0);
}
phead->pnext = NULL; // 头节点初始无后续节点,链表为空
return phead;
}
3. 第三步:新增节点(尾插法,最常用)
新增节点是向链表末尾添加数据,流程为 “创建新节点→遍历找到尾节点→链接新节点”:
c
运行
// 尾插法新增节点(向链表末尾添加数据)
void addNodeTail(struct Node *phead, int data) {
if (phead == NULL) {
printf("链表未初始化\n");
return;
}
// 1. 创建新节点
struct Node *newNode = createNode(data);
if (newNode == NULL) return;
// 2. 遍历找到链表尾节点(pnext为NULL的节点)
struct Node *ptemp = phead; // 第三方指针,避免修改头节点
while (ptemp->pnext != NULL) {
ptemp = ptemp->pnext; // 指针后移,直到尾节点
}
// 3. 链接新节点(尾节点指向新节点)
ptemp->pnext = newNode;
printf("新增节点:%d\n", data);
}
4. 第四步:查询节点(按值查找)
查询节点是根据数据值找到目标节点,流程为 “遍历链表→对比数据→返回结果”:
c
运行
// 按值查询节点,返回节点地址(未找到返回NULL)
struct Node *searchNodeByValue(struct Node *phead, int target) {
if (phead == NULL || phead->pnext == NULL) {
printf("链表为空\n");
return NULL;
}
struct Node *ptemp = phead->pnext; // 从第一个有效节点开始遍历
while (ptemp != NULL) {
if (ptemp->data == target) {
printf("找到目标节点:%d\n", target);
return ptemp;
}
ptemp = ptemp->pnext;
}
printf("未找到目标节点:%d\n", target);
return NULL;
}
5. 第五步:修改节点(按值修改)
修改节点需先找到目标节点,再修改其数据域:
c
运行
// 按值修改节点数据
void modifyNodeByValue(struct Node *phead, int oldVal, int newVal) {
// 先查询目标节点
struct Node *targetNode = searchNodeByValue(phead, oldVal);
if (targetNode != NULL) {
targetNode->data = newVal;
printf("修改成功:%d → %d\n", oldVal, newVal);
}
}
6. 第六步:删除节点(按值删除)
删除节点是核心难点,需确保链表不中断,流程为 “查找目标节点的前驱节点→重新链接→释放内存”:
c
运行
// 按值删除节点
void deleteNodeByValue(struct Node *phead, int target) {
if (phead == NULL || phead->pnext == NULL) {
printf("链表为空,无法删除\n");
return;
}
struct Node *ptemp = phead; // ptemp最终指向目标节点的前驱节点
// 遍历查找目标节点的前驱节点
while (ptemp->pnext != NULL && ptemp->pnext->data != target) {
ptemp = ptemp->pnext;
}
// 未找到目标节点
if (ptemp->pnext == NULL) {
printf("未找到要删除的节点:%d\n", target);
return;
}
// 1. 保存要删除的节点
struct Node *pdel = ptemp->pnext;
// 2. 重新链接链表(前驱节点指向目标节点的后续节点)
ptemp->pnext = ptemp->pnext->pnext;
// 3. 释放删除节点的内存
free(pdel);
pdel = NULL;
printf("删除节点成功:%d\n", target);
}
7. 第七步:打印链表(验证操作结果)
打印链表用于查看当前链表结构,验证操作正确性:
c
运行
// 打印链表所有有效节点
void printLinkList(struct Node *phead) {
if (phead == NULL || phead->pnext == NULL) {
printf("链表为空\n");
return;
}
struct Node *ptemp = phead->pnext;
printf("链表节点:");
while (ptemp != NULL) {
printf("%d → ", ptemp->data);
ptemp = ptemp->pnext;
}
printf("NULL\n");
}
8. 第八步:清空链表(释放所有节点)
链表使用完毕后,需释放所有节点内存,避免内存泄漏:
c
运行
// 清空链表(保留头节点,释放所有有效节点)
void clearLinkList(struct Node *phead) {
if (phead == NULL) return;
struct Node *ptemp = phead->pnext;
struct Node *pdel = NULL;
while (ptemp != NULL) {
pdel = ptemp; // 保存当前节点
ptemp = ptemp->pnext; // 指针后移
free(pdel); // 释放当前节点
pdel = NULL;
}
phead->pnext = NULL; // 头节点指针置NULL,链表恢复为空
printf("链表清空成功\n");
}
三、完整实战:链表操作流程演示
将上述函数整合,实现 “初始化→新增→查询→修改→删除→清空” 的完整流程:
c
运行
int main() {
// 1. 初始化链表
struct Node *phead = initLinkList();
printf("===== 初始化链表 =====\n");
printLinkList(phead);
// 2. 新增节点
printf("\n===== 新增节点 =====\n");
addNodeTail(phead, 10);
addNodeTail(phead, 20);
addNodeTail(phead, 30);
addNodeTail(phead, 40);
printLinkList(phead);
// 3. 查询节点
printf("\n===== 查询节点 =====\n");
searchNodeByValue(phead, 20);
searchNodeByValue(phead, 50);
// 4. 修改节点
printf("\n===== 修改节点 =====\n");
modifyNodeByValue(phead, 30, 35);
printLinkList(phead);
// 5. 删除节点
printf("\n===== 删除节点 =====\n");
deleteNodeByValue(phead, 20);
printLinkList(phead);
// 6. 清空链表
printf("\n===== 清空链表 =====\n");
clearLinkList(phead);
printLinkList(phead);
// 释放头节点内存(程序结束前)
free(phead);
phead = NULL;
return 0;
}
编译与运行结果
bash
运行
gcc link_list.c -o link_list
./link_list
plaintext
===== 初始化链表 =====
链表为空
===== 新增节点 =====
新增节点:10
新增节点:20
新增节点:30
新增节点:40
链表节点:10 → 20 → 30 → 40 → NULL
===== 查询节点 =====
找到目标节点:20
未找到目标节点:50
===== 修改节点 =====
找到目标节点:30
修改成功:30 → 35
链表节点:10 → 20 → 35 → 40 → NULL
===== 删除节点 =====
删除节点成功:20
链表节点:10 → 35 → 40 → NULL
===== 清空链表 =====
链表清空成功
链表为空
四、嵌入式开发注意事项
- 内存管理:嵌入式设备内存有限,必须确保 “malloc 分配的内存都有 free 释放”,避免内存泄漏导致系统崩溃;
- 指针安全:所有指针操作前需判空,避免野指针;删除节点后需将指针置 NULL,防止二次释放;
- 效率优化:单链表的查询、删除操作时间复杂度为 O (n),若数据量较大,可使用双向链表或二叉搜索树优化;
- 静态内存替代:若嵌入式设备不支持动态内存分配(如部分单片机),可预先定义节点数组(静态内存池),替代
malloc和free; - 多线程安全:若在多线程环境中操作链表,需添加互斥锁(如
pthread_mutex_t),避免并发访问导致链表混乱。
五、总结
链表是嵌入式 C 语言的核心数据结构,核心要点可总结为:
- 本质是 “结构体 + 指针” 的组合,通过指针串联节点,实现动态存储;
- 核心操作围绕 “节点创建→链接→查询→修改→删除→内存释放” 展开,关键是指针的正确操作;
- 带头节点的设计能简化边界处理,是嵌入式开发的首选;
- 嵌入式场景中需重点关注内存管理和指针安全,避免内存泄漏和野指针问题。
掌握链表的实现与操作,能轻松应对嵌入式开发中动态数据存储的需求,为后续学习队列、栈、二叉树等复杂数据结构打下坚实基础。我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧,一起进步!