链表(Linked List)

链表(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_registerebtn.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