链表(Linked List)
不用提前知道有多少个元素,动态串联节点。
ebtn 的动态按键btn_dyn_head就是单链表。
一、为什么需要它
数组的问题:大小必须编译时确定。
ebtn_btn_t btns[8]; // 最多8个,多了没地方放,少了浪费内存
链表的解法:每个节点自己带一个指针,指向下一个节点,想加就加:
btns[0] → btns[1] → btns[2] → NULL
node node node (链表结束标志)
二、核心结构
typedef struct node {
int data; // 有效数据(可以是任意类型)
struct node *next; // 指向下一个节点
} node_t;
next 是关键——节点通过指针串起来,内存上可以不连续:
内存地址: 0x200 0x340 0x180
┌──────┐ ┌──────┐ ┌──────┐
│data=1│ │data=2│ │data=3│
│next──┼──→│next──┼──→│next │→ NULL
└──────┘ └──────┘ └──────┘
只需要记住头节点地址,就能遍历整个链表。
三、ebtn 里的链表
ebtn.h:266-270:
typedef struct ebtn_btn_dyn {
struct ebtn_btn_dyn *next; // 链表指针
ebtn_btn_t btn; // 有效数据
} ebtn_btn_dyn_t;
ebtn_t 里存头指针:
ebtn_btn_dyn_t *btn_dyn_head; // 指向链表第一个节点,NULL 表示空链表
遍历(ebtn.c:316):
for (target = ebtobj->btn_dyn_head; target; target = target->next)
{
// target 依次指向每个动态按键
}
target = target->next 就是"移到下一个节点",target == NULL 时循环结束。
四、基本操作实现
遍历
void list_print(node_t *head)
{
node_t *curr = head;
while (curr != NULL) {
printf("%d ", curr->data);
curr = curr->next;
}
}
头插(新节点插到最前面)
void list_push_front(node_t **head, node_t *new_node)
{
new_node->next = *head; // 新节点的 next 指向原来的头
*head = new_node; // 头指针更新为新节点
}
插入前:head → [A] → [B] → NULL
插入后:head → [NEW] → [A] → [B] → NULL
尾插(新节点插到最后)
void list_push_back(node_t **head, node_t *new_node)
{
new_node->next = NULL;
if (*head == NULL) { // 空链表
*head = new_node;
return;
}
node_t *curr = *head;
while (curr->next != NULL) { // 找到最后一个节点
curr = curr->next;
}
curr->next = new_node; // 接上去
}
ebtn 的 ebtn_register(ebtn.c:505)就是这个逻辑,同时还检查了重复和上限。
删除指定节点
void list_remove(node_t **head, node_t *target)
{
if (*head == target) { // 删的是头节点
*head = target->next;
return;
}
node_t *curr = *head;
while (curr->next != NULL) {
if (curr->next == target) {
curr->next = target->next; // 跳过 target
return;
}
curr = curr->next;
}
}
删除前:head → [A] → [B] → [C] → NULL
删 B: head → [A] ──────→ [C] → NULL
[B] 的内存还在,只是没人指向它了
五、嵌入式链表的特殊做法:不用 malloc
堆内存(malloc)在嵌入式里危险:碎片化、不可预测。
常见做法:静态分配节点,链表只负责串联。
// 静态分配好的节点池
static ebtn_btn_dyn_t dyn_btn_pool[4];
// 使用时直接取池里的节点,不用 malloc
ebtn_register(&dyn_btn_pool[0]);
ebtn 的动态按键就是这么设计的——ebtn_btn_dyn_t 由用户静态定义,ebtn_register 只负责把它串进链表。
六、单链表 vs 双链表
| 单链表 | 双链表 | |
|---|---|---|
| 每个节点 | *next |
*next + *prev |
| 内存 | 少 | 多一个指针 |
| 向前遍历 | 不支持 | 支持 |
| 删除节点 | 需要找前驱 | O(1) 直接断链 |
| 嵌入式使用 | 多(够用且省内存) | 少(除非需要双向) |
ebtn 用的是单链表,够用。
七、链表 vs 数组
| 数组 | 链表 | |
|---|---|---|
| 大小 | 编译时固定 | 运行时动态 |
| 随机访问 | O(1),arr[i] |
O(n),要遍历 |
| 插入/删除 | O(n),要移动元素 | O(1),改指针 |
| 内存连续 | 是(cache 友好) | 否 |
| 嵌入式适用 | 大多数情况 | 数量不定时 |
判断标准:元素数量编译时确定 → 用数组;数量运行时才知道 → 用链表。
八、在哪能看到这个 pattern
| 系统 | 位置 |
|---|---|
| ebtn | btn_dyn_head,动态按键链表 |
| FreeRTOS | 就绪队列、延时队列,全是链表 |
| Linux 内核 | list_head,侵入式双链表(经典设计) |
| LVGL | lv_obj 的子控件列表 |
| lwIP | 数据包缓冲区 pbuf 链 |