【嵌入式 C 语言实战】链表从入门到精通:存储原理 + 核心操作 + 函数详解

0 阅读9分钟

【嵌入式 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

===== 清空链表 =====
链表清空成功
链表为空

四、嵌入式开发注意事项

  1. 内存管理:嵌入式设备内存有限,必须确保 “malloc 分配的内存都有 free 释放”,避免内存泄漏导致系统崩溃;
  2. 指针安全:所有指针操作前需判空,避免野指针;删除节点后需将指针置 NULL,防止二次释放;
  3. 效率优化:单链表的查询、删除操作时间复杂度为 O (n),若数据量较大,可使用双向链表或二叉搜索树优化;
  4. 静态内存替代:若嵌入式设备不支持动态内存分配(如部分单片机),可预先定义节点数组(静态内存池),替代mallocfree
  5. 多线程安全:若在多线程环境中操作链表,需添加互斥锁(如pthread_mutex_t),避免并发访问导致链表混乱。

五、总结

链表是嵌入式 C 语言的核心数据结构,核心要点可总结为:

  1. 本质是 “结构体 + 指针” 的组合,通过指针串联节点,实现动态存储;
  2. 核心操作围绕 “节点创建→链接→查询→修改→删除→内存释放” 展开,关键是指针的正确操作;
  3. 带头节点的设计能简化边界处理,是嵌入式开发的首选;
  4. 嵌入式场景中需重点关注内存管理和指针安全,避免内存泄漏和野指针问题。

掌握链表的实现与操作,能轻松应对嵌入式开发中动态数据存储的需求,为后续学习队列、栈、二叉树等复杂数据结构打下坚实基础。我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧,一起进步!