目录
前言:
一、单向链表核心本质与应用场景
1. 什么是单向链表
2. 解决的核心痛点
3. 典型工业落地场景
二、核心实现原理
1. 带头结点设计
2. 单向遍历机制
3. 静态节点优先
三、工业级设计规范
1. 封装设计
2. 接口设计
3. 鲁棒约束
4. 线程安全
四、完整可复用源码
1.slist.h
2.slist.c
五、实战演示
六、进阶优化方向
七、面试考点与易错坑点
1.面试问答
2.常见坑
总结
前言:
嵌入式很多资源紧张的 8 位单片机,RAM 极小,不需要反向遍历场景,双向链表prev指针会额外占用内存。单向链表结构最简、内存开销最小,适合简易节点管理。
很多新手分不清单向链表与双向链表适用场景,盲目全部使用双向链表;本篇实现极简工业级单向链表,支持静态节点、无动态内存强制依赖,接口精简,适合多路设备、简易任务列表、日志节点等轻量级管理场景。
一、单向链表核心本质与应用场景
1. 什么是单向链表
单向链表每个节点仅包含后继 next 指针,只能够从头节点向尾部单向遍历;不存在前驱指针,内存占用相比双向链表节省一半指针空间。
特性:节点无需连续内存;支持动态增删;不支持反向遍历;查找指定节点删除时,需要从头遍历。
2. 解决的核心痛点
- 解决小型单片机内存资源紧张问题:省去 prev 指针,减少 RAM 占用。
- 解决数组长度固定、扩容难:动态挂载节点,不受预设数组大小限制。
- 解决简易节点管理重复造轮子:多路 IO、简易任务、临时日志统一管理。
- 规避频繁 malloc 碎片:支持静态定义节点,全程使用静态内存。
3. 典型工业落地场景
- 简易多路传感器节点登记管理。
- 临时日志、告警信息临时挂载链表缓存。
- 简易任务列表,顺序轮询执行。
- 串口会话简易登记(不需要反向查找场景)。
- 参数条目简易遍历管理。
二、核心实现原理
1. 带头结点设计
采用独立头节点,头结点不存储业务数据,统一空链表、首尾节点边界处理逻辑,消除大量 if 分支,嵌入式标准写法。
2. 单向遍历机制
只能由 head 依次顺着 next 向后访问节点;
删除目标节点时,需要保存前驱节点指针。
3. 静态节点优先
组件不强制动态堆分配,节点定义为全局 / 局部静态变量,杜绝内存碎片、分配失败风险。
三、工业级设计规范
1. 封装设计
基础链表节点结构体通用,业务结构体内嵌链表节点,不需要内存拷贝。
2. 接口设计
| 接口 | 功能说明 |
|---|---|
| slist_init | 初始化链表头结点 |
| slist_add_head | 头部插入节点 |
| slist_add_tail | 尾部插入节点 |
| slist_remove | 移除指定节点 |
| slist_is_empty | 判断链表为空 |
| slist_foreach | 单向遍历所有节点 |
3. 鲁棒约束
- 空指针全部校验;禁止同一节点重复挂载;
- 节点移除后置空 next 指针,避免野指针;
- 纯 C 无第三方依赖,裸机通用。
4. 线程安全
单线程天然安全;
多线程并发操作链表,外部增加关中断或者互斥锁保护。
四、完整可复用源码
1.slist.h
#ifndef SLIST_H #define SLIST_H #include <stddef.h> #include <stdbool.h> #ifdef __cplusplus extern "C" { #endif //单向链表基础节点 typedef struct slist_node { struct slist_node *next; } slist_node_t; /** * @brief 初始化单向链表头 */ void slist_init(slist_node_t *head); /** * @brief 头部插入节点 */ void slist_add_head(slist_node_t *head, slist_node_t *node); /** * @brief 尾部插入节点 */ void slist_add_tail(slist_node_t *head, slist_node_t *node); /** * @brief 删除指定节点 */ bool slist_remove(slist_node_t *head, slist_node_t *node); /** * @brief 判断链表是否为空 */ bool slist_is_empty(slist_node_t *head); // 通过链表节点获取宿主结构体 #define slist_container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) //单向遍历宏 #define slist_foreach(pos, head) \ for (pos = (head)->next; pos != NULL; pos = pos->next) #ifdef __cplusplus } #endif #endif2.slist.c
#include "slist.h" void slist_init(slist_node_t *head) { if(head == NULL) return; head->next = NULL; } void slist_add_head(slist_node_t *head, slist_node_t *node) { if(head == NULL || node == NULL) return; node->next = head->next; head->next = node; } void slist_add_tail(slist_node_t *head, slist_node_t *node) { if(head == NULL || node == NULL) return; slist_node_t *p = head; while(p->next != NULL) { p = p->next; } node->next = NULL; p->next = node; } bool slist_remove(slist_node_t *head, slist_node_t *node) { if(head == NULL || node == NULL || slist_is_empty(head)) return false; slist_node_t *prev = head; slist_node_t *curr = head->next; while(curr != NULL) { if(curr == node) { prev->next = curr->next; node->next = NULL; return true; } prev = curr; curr = curr->next; } return false; } bool slist_is_empty(slist_node_t *head) { if(head == NULL) return true; return head->next == NULL; }五、实战演示
#include <stdio.h> #include "slist.h" //业务节点示例 typedef struct { uint8_t dev_id; slist_node_t node; } dev_item_t; dev_item_t dev1, dev2, dev3; int main(void) { slist_node_t slist_head; slist_init(&slist_head); dev1.dev_id = 1; dev2.dev_id = 2; dev3.dev_id = 3; slist_add_tail(&slist_head, &dev1.node); slist_add_tail(&slist_head, &dev2.node); slist_add_tail(&slist_head, &dev3.node); slist_node_t *pos; slist_foreach(pos, &slist_head) { dev_item_t *item = slist_container_of(pos, dev_item_t, node); printf("设备ID:%d\n", item->dev_id); } slist_remove(&slist_head, &dev2.node); printf("删除设备2完成\n"); return 0; }六、进阶优化方向
- 增加链表节点计数,不需要遍历即可获取节点总数
- 缓存尾指针,规避尾插每次从头遍历,提升尾部插入效率
- 支持按条件查找节点封装通用接口
七、面试考点与易错坑点
1.面试问答
Q1:单向链表与双向链表怎么选型?
答:只需要正向遍历、追求最小内存占用、无频繁随机删除场景 → 单向链表;需要快速删除、双向遍历、频繁随机移除节点 → 双向链表。
Q2:单向链表删除节点为什么需要前驱指针?
答:节点本身无法访问上一级节点,必须遍历保存前驱,修改前驱 next 指针。
Q3:单向链表尾部插入效率短板如何优化?
答:可以额外保存尾指针,不需要每次遍历到链表末尾。
2.常见坑
- 节点移除不置空 next 指针,引发野指针;
- 重复添加同一个节点,形成环形链表死循环;
- 遍历时直接删除当前遍历节点,导致遍历断链崩溃。
总结
- 单向链表是资源受限单片机首选动态容器,结构极简、内存开销最低。
- 在不需要反向遍历的场景下,相比双向链表拥有天然 RAM 优势。
- 适合简易设备管理、任务列表等轻量级业务,是嵌入式底层基础数据结构。
创作不易,如果对你有帮助,欢迎点赞、收藏、转发。