【嵌入式 C 语言实战】单链表的完整实现与核心操作详解
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,线性表是最基础的数据结构之一,而单链表凭借其不依赖连续内存、动态扩展的特性,在内存资源紧张的嵌入式设备中应用广泛(如串口数据缓存、设备链表管理等)。今天就带大家从零实现一个完整的单链表,涵盖初始化、增删改查、反转、清空等所有核心操作,全程附可直接运行的 C 语言代码。
一、单链表的核心概念(嵌入式开发视角)
1. 什么是单链表?
单链表是一种链式存储的线性表,它由一个个节点串联而成,每个节点包含两个部分:
- 数据域:存储实际业务数据(如设备 ID、传感器数值等);
- 指针域:存储下一个节点的内存地址,实现节点间的串联。
与顺序表相比,单链表无需占用连续的内存空间,新增 / 删除节点无需移动大量数据,更适合嵌入式系统中 "数据量不固定、内存碎片化" 的场景。
2. 头节点的设计思路
本文实现的单链表采用带头节点的设计(嵌入式开发常用),头节点不存储有效业务数据,仅用于标识链表的起始位置,好处有二:
- 统一链表的操作逻辑,避免空链表和非空链表的边界判断差异;
- 简化表头插入、删除操作,无需额外处理头指针的特殊情况。
二、单链表的前期准备(结构体定义与函数声明)
1. 节点结构体定义
首先定义单链表的节点结构体,包含数据域和指针域:
c
运行
arduino
#include<stdio.h>
#include<stdlib.h>
// 单链表节点结构体
typedef struct Node {
int data; // 数据域:存储int类型数据(嵌入式可替换为设备结构体等)
struct Node *next; // 指针域:指向后续节点
}Node;
2. 核心操作函数声明
本文实现单链表的 12 个核心操作,覆盖开发中的常见需求,函数声明如下:
c
运行
arduino
Node *InitLinkList();// 初始化链表(创建头节点)
void ClearLinkList(Node *head);// 清空链表(释放所有有效节点,保留头节点)
void AddNodehead(Node *head,int data);// 表头插入节点
void AddNodeTail(Node *head,int data);// 表尾插入节点
void InsertNode(Node *head,int data,int pos);// 指定位置插入节点
void DeleteNodeByValue(Node *head, int data);// 按值删除节点
void DeleteNodeByIndex(Node *head, int index);// 按索引删除节点
int SearchNodeByValue(Node *head,int value);// 按值查询节点位置
int SearchNodeAtPos(Node *head,int pos);// 按位置查询节点值
void ModifyNode(Node *head, int pos, int data);// 修改指定位置节点数据
void PrintLinkedList(Node *head);// 打印链表所有节点
int GetLinkedListLength(Node *head);// 获取链表长度
void ReverseLinkedList(Node *head);// 反转链表
三、单链表的核心操作实现(逐函数详解)
1. 链表初始化(InitLinkList)
初始化链表的核心是创建一个头节点,初始化其指针域为NULL,标识链表为空。
c
运行
scss
Node *InitLinkList()// 初始化链表(创建头节点)
{
// 为头节点分配堆内存(嵌入式需注意内存分配失败处理)
Node *head=(Node*)malloc(sizeof(Node));
if(head==NULL)
{
printf("malloc error: 头节点内存分配失败\n");
exit(0);
}
head->data = 0; // 头节点数据域无意义,可置0
head->next = NULL; // 头节点后续无节点,指针置NULL
return head;
}
嵌入式注意点 :在资源受限的嵌入式设备中,malloc可能分配失败,必须添加判空逻辑,避免野指针导致系统崩溃。
2. 表头 / 表尾插入节点(AddNodehead/AddNodeTail)
(1)表头插入(AddNodehead)
表头插入采用 "前插法",新节点直接插入到头节点之后,步骤为:创建新节点→新节点指向头节点原后续节点→头节点指向新节点。
c
运行
ini
void AddNodehead(Node *head,int data)
{
if(head==NULL)
{
printf("head is null: 链表未初始化\n");
return;
}
// 1. 创建新节点并初始化
Node *p=(Node*)malloc(sizeof(Node));
if(p==NULL)
{
printf("malloc error: 新节点内存分配失败\n");
return;
}
p->data=data;
// 2. 新节点指向头节点的原后续节点
p->next=head->next;
// 3. 头节点指向新节点,完成插入
head->next=p;
}
特点:表头插入的时间复杂度为 O (1),效率极高,适合需要 "先进后出" 的场景。
(2)表尾插入(AddNodeTail)
表尾插入采用 "后插法",需要先遍历到链表末尾,再将新节点插入到末尾节点之后,步骤为:创建新节点→遍历到表尾→表尾节点指向新节点。
c
运行
ini
void AddNodeTail(Node *head,int data)
{
if(head==NULL)
{
printf("head is null: 链表未初始化\n");
return;
}
// 1. 创建新节点并初始化
Node *p=(Node*)malloc(sizeof(Node));
if(p==NULL)
{
printf("malloc error: 新节点内存分配失败\n");
return;
}
p->data=data;
p->next=NULL; // 表尾节点后续无节点,指针置NULL
// 2. 遍历到链表末尾(找到next为NULL的节点)
Node *q=head;
while(q->next!=NULL)
{
q=q->next;
}
// 3. 表尾节点指向新节点,完成插入
q->next=p;
}
特点:表尾插入的时间复杂度为 O (n)(需遍历链表),适合需要 "先进先出" 的场景。
3. 指定位置插入与修改(InsertNode/ModifyNode)
(1)指定位置插入(InsertNode)
指定位置插入的核心是找到目标位置的前一个节点,再执行插入操作,避免数据覆盖。
c
运行
ini
void InsertNode(Node *head,int data,int pos)
{
if(head==NULL)
{
printf("head is null: 链表未初始化\n");
return;
}
if(pos<1)
{
printf("pos error: 插入位置不能小于1\n");
return;
}
// 1. 找到第pos-1个节点(插入位置的前一个节点)
Node *p=head;
int cur=0;
while(p->next!=NULL&&cur<pos-1)
{
p=p->next;
cur++;
}
// 2. 判断插入位置是否合法(超出链表长度)
if(cur<pos-1)
{
printf("pos error: 插入位置超出链表长度\n");
return;
}
// 3. 创建新节点并完成插入
Node *q=(Node *)malloc(sizeof(Node));
if(q==NULL)
{
printf("malloc error: 新节点内存分配失败\n");
return;
}
q->data=data;
q->next=p->next;
p->next=q;
}
(2)指定位置修改(ModifyNode)
指定位置修改无需创建新节点,只需找到目标节点,直接修改其数据域即可。
c
运行
arduino
void ModifyNode(Node *head, int pos, int data)
{
if(head==NULL||head->next==NULL)
{
printf("链表为空,无法修改\n");
return;
}
if(pos<1)
{
printf("pos is error: 修改位置不能小于1\n");
return;
}
// 1. 找到目标位置的节点
Node *p=head->next;
int cur=1;
while(p!=NULL&&cur<pos)
{
p=p->next;
cur++;
}
// 2. 判断修改位置是否合法
if(p==NULL)
{
printf("链表长度不够,修改位置超出范围\n");
return;
}
// 3. 修改节点数据域
p->data=data;
printf("第%d个节点修改成功为%d\n",pos,data);
}
4. 按值 / 按索引删除(DeleteNodeByValue/DeleteNodeByIndex)
删除操作的核心是:找到目标节点的前一个节点→修改指针指向→释放目标节点内存(避免内存泄漏)。
(1)按值删除(DeleteNodeByValue)
c
运行
ini
void DeleteNodeByValue(Node *head, int data)
{
if(head==NULL||head->next==NULL)
{
printf("链表为空,无法删除\n");
return;
}
// 1. 找到值为data的节点的前一个节点
Node *p=head;
while(p->next!=NULL&&p->next->data!=data)
{
p=p->next;
}
// 2. 判断是否找到目标节点
if(p->next==NULL)
{
printf("not find: 未找到要删除的值\n");
return;
}
// 3. 修改指针并释放目标节点内存
Node *q=p->next;
p->next=q->next;
free(q);
q=NULL; // 避免野指针
printf("delete success: 成功删除值为%d的节点\n",data);
}
(2)按索引删除(DeleteNodeByIndex)
c
运行
ini
void DeleteNodeByIndex(Node *head, int index)
{
if(head==NULL||head->next==NULL)
{
printf("链表为空,无法删除\n");
return;
}
if(index<1)
{
printf("index error: 删除索引不能小于1\n");
return;
}
// 1. 找到第index个节点的前一个节点
Node *p=head;
int pos=0;
while(p->next!=NULL&&index>1)
{
p=p->next;
index--;
pos++;
}
// 2. 判断删除索引是否合法
if(p->next==NULL)
{
printf("index error: 删除索引超出链表长度\n");
return;
}
// 3. 修改指针并释放目标节点内存
Node *q=p->next;
p->next=q->next;
free(q);
q=NULL; // 避免野指针
printf("delete success: 成功删除索引为%d的节点\n",pos+1);
}
嵌入式注意点 :嵌入式系统中内存资源宝贵,删除节点后必须调用free释放内存,否则会导致内存泄漏,长期运行可能使系统崩溃。
5. 链表查询与辅助操作(SearchNodeByValue/PrintLinkedList等)
(1)按值查询与按位置查询
c
运行
perl
int SearchNodeByValue(Node *head,int value)
{
if(head==NULL||head->next==NULL)
{
printf("list is empty: 链表为空\n");
return -1;
}
Node *p=head->next;
int pos=1;
while(p!=NULL)
{
if(p->data==value)
{
printf("%d is in position %d\n",value,pos);
return pos;
}
p=p->next;
pos++;
}
printf("%d is not in list\n",value);
return -1;
}
int SearchNodeAtPos(Node *head,int pos)
{
if(head==NULL||head->next==NULL)
{
printf("list is empty: 链表为空\n");
return -1;
}
if(pos<1)
{
printf("pos is error: 查询位置不能小于1\n");
return -1;
}
int cur=1;
Node *p=head->next;
while(p!=NULL&&cur<pos)
{
p=p->next;
cur++;
}
if(p==NULL)
{
printf("pos is error: 查询位置超出链表长度\n");
return -1;
}
printf("第%d个节点的数据为:%d\n", pos, p->data);
return p->data;
}
(2)打印链表与获取链表长度
c
运行
ini
void PrintLinkedList(Node *head)
{
if(head==NULL)
{
printf("链表为空\n");
return;
}
Node *p=head->next;
if(p==NULL)
{
printf("链表为空\n");
return;
}
printf("链表节点数据:");
while(p!=NULL)
{
printf("%d\t",p->data);
p=p->next;
}
printf("\n");
}
int GetLinkedListLength(Node *head)
{
if(head==NULL)
{
printf("链表为空\n");
return 0;
}
Node *p=head->next;
int len=0;
while(p!=NULL)
{
len++;
p=p->next;
}
return len;
}
6. 链表反转与清空(ReverseLinkedList/ClearLinkList)
(1)链表反转(原地反转,无需额外内存)
链表反转是嵌入式面试高频考点,本文采用 "头插法反转",无需额外分配内存,效率较高。
c
运行
ini
void ReverseLinkedList(Node *head)
{
// 边界判断:空链表或只有一个节点,无需反转
if(head==NULL||head->next==NULL||head->next->next==NULL)
{
return;
}
Node *p=head->next; // 指向当前需要反转的节点
Node *q=p->next; // 指向p的后续节点,防止链表断裂
while(q!=NULL)
{
p->next=q->next; // p指向q的后续节点,保留未反转部分
q->next=head->next; // q指向已反转部分的表头
head->next=q; // 头节点指向q,完成q的反转
q=p->next; // q移动到下一个需要反转的节点
}
printf("链表反转成功\n");
}
(2)链表清空(保留头节点,释放所有有效节点)
c
运行
ini
void ClearLinkList(Node *head)// 清除链表(释放所有有效节点,保留头节点)
{
if(head==NULL)
{
return;
}
Node *p=head->next;
while(p!=NULL)
{
Node*q=p;
p=p->next;
free(q); // 逐个释放有效节点
q=NULL;
}
head->next=NULL; // 头节点指针置NULL,标识链表为空
printf("链表清空成功\n");
}
四、单链表的完整测试(main函数)
下面通过一个完整的测试流程,验证所有单链表操作的正确性,代码可直接编译运行:
c
运行
scss
int main()
{
Node *head=InitLinkList();
printf("=====步骤1:初始化链表=====\n");
PrintLinkedList(head);
printf("链表长度为:%d\n",GetLinkedListLength(head));
AddNodehead(head,1);
AddNodehead(head,2);
AddNodehead(head,3);
printf("\n=====步骤2:头部添加节点=====\n");
PrintLinkedList(head);
printf("链表长度为:%d\n",GetLinkedListLength(head));
AddNodeTail(head,4);
AddNodeTail(head,5);
printf("\n=====步骤3:尾部添加节点=====\n");
PrintLinkedList(head);
printf("链表长度为:%d\n",GetLinkedListLength(head));
InsertNode(head,35,3); // 修正原代码参数顺序:数据35,位置3
printf("\n=====步骤4:指定位置插入节点=====\n");
PrintLinkedList(head);
printf("链表长度为:%d\n",GetLinkedListLength(head));
int targetValue = 35;
int targetPos = 4;
printf("\n=====步骤5:查询操作=====\n");
int pos = SearchNodeByValue(head,targetValue);
if(pos!=-1)
{
printf("目标值%d在链表中的位置为:%d\n",targetValue,pos);
}
int data = SearchNodeAtPos(head,targetPos);
if(data!=-1)
{
printf("链表中位置为%d的元素的值为:%d\n",targetPos,data);
}
ModifyNode(head,4,15);
printf("\n=====步骤6:修改操作=====\n");
PrintLinkedList(head);
ReverseLinkedList(head);
printf("\n=====步骤7:反转操作=====\n");
PrintLinkedList(head); // 修正原代码函数名拼写错误:PrintLinkedList
printf("当前链表长度为:%d\n",GetLinkedListLength(head));
DeleteNodeByValue(head,35);
printf("\n=====步骤8:按值删除=====\n");
PrintLinkedList(head);
printf("当前链表长度为:%d\n",GetLinkedListLength(head));
DeleteNodeByIndex(head,2);
printf("\n=====步骤9:按索引删除=====\n");
PrintLinkedList(head);
printf("当前链表长度为:%d\n",GetLinkedListLength(head));
ClearLinkList(head);
free(head); // 释放头节点内存(程序结束前)
head = NULL;
return 0;
}
注意:修正原代码的 2 处小问题
- 原代码
InsertNode(head,3,35)参数顺序颠倒,应改为InsertNode(head,35,3)(数据在前,位置在后); - 原代码反转后调用
PrintLinkList(拼写错误),应改为PrintLinkedList。
五、编译运行与结果说明
1. 编译命令(Linux/macOS)
bash
运行
bash
gcc linked_list.c -o linked_list
./linked_list
2. 预期运行结果
plaintext
makefile
=====步骤1:初始化链表=====
链表为空
链表长度为:0
=====步骤2:头部添加节点=====
链表节点数据:3 2 1
链表长度为:3
=====步骤3:尾部添加节点=====
链表节点数据:3 2 1 4 5
链表长度为:5
=====步骤4:指定位置插入节点=====
链表节点数据:3 2 35 1 4 5
链表长度为:6
=====步骤5:查询操作=====
35 is in position 3
目标值35在链表中的位置为:3
第4个节点的数据为:1
链表中位置为4的元素的值为:1
=====步骤6:修改操作=====
第4个节点修改成功为15
链表节点数据:3 2 35 15 4 5
=====步骤7:反转操作=====
链表反转成功
链表节点数据:5 4 15 35 2 3
当前链表长度为:6
=====步骤8:按值删除=====
delete success: 成功删除值为35的节点
链表节点数据:5 4 15 2 3
当前链表长度为:5
=====步骤9:按索引删除=====
delete success: 成功删除索引为2的节点
链表节点数据:5 15 2 3
当前链表长度为:4
链表清空成功
六、嵌入式开发中的单链表应用与优化建议
1. 典型应用场景
- 串口接收 / 发送缓存:动态存储不定长的串口数据,避免固定数组的内存浪费;
- 设备管理链表:嵌入式设备中多个外设(如传感器、串口)可通过单链表管理,便于动态添加 / 删除设备;
- 中断服务程序中的数据缓存:单链表在中断中操作简单,无需移动数据,适合高速数据采集。
2. 优化建议
- 使用静态内存替代
malloc:嵌入式系统中malloc可能导致内存碎片化,可预先分配静态节点数组,实现 "内存池" 机制; - 增加线程安全保护 :若在多线程 / 中断中操作链表,需添加互斥锁(如
OSMutex)或关中断保护,避免链表操作混乱; - 优化查询效率:对于频繁查询的场景,可采用双向链表或有序链表,提升查询效率;
- 添加节点计数:在头节点的数据域中存储链表长度,避免每次获取长度都遍历链表(时间复杂度从 O (n) 优化为 O (1))。
七、总结
- 单链表的核心是节点的指针操作,所有增删改查操作都围绕 "修改指针指向" 和 "释放内存" 展开;
- 带头节点的单链表能简化边界处理,是嵌入式开发中的首选设计;
- 单链表的优势是动态扩展、不依赖连续内存,劣势是不支持随机访问,查询效率为 O (n);
- 嵌入式开发中使用单链表,需重点关注内存泄漏 和线程安全问题,确保系统稳定运行。
作为嵌入式开发者,掌握单链表的实现是数据结构的入门基础,后续还可以深入学习双向链表、循环链表等复杂结构,为后续开发更复杂的嵌入式系统打下坚实基础。我是学嵌入式的小杨同学,关注我,一起解锁更多嵌入式 C 语言实战技巧!