【嵌入式 C 语言实战】单链表的完整实现与核心操作详解

【嵌入式 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 处小问题

  1. 原代码InsertNode(head,3,35)参数顺序颠倒,应改为InsertNode(head,35,3)(数据在前,位置在后);
  2. 原代码反转后调用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. 优化建议

  1. 使用静态内存替代malloc :嵌入式系统中malloc可能导致内存碎片化,可预先分配静态节点数组,实现 "内存池" 机制;
  2. 增加线程安全保护 :若在多线程 / 中断中操作链表,需添加互斥锁(如OSMutex)或关中断保护,避免链表操作混乱;
  3. 优化查询效率:对于频繁查询的场景,可采用双向链表或有序链表,提升查询效率;
  4. 添加节点计数:在头节点的数据域中存储链表长度,避免每次获取长度都遍历链表(时间复杂度从 O (n) 优化为 O (1))。

七、总结

  1. 单链表的核心是节点的指针操作,所有增删改查操作都围绕 "修改指针指向" 和 "释放内存" 展开;
  2. 带头节点的单链表能简化边界处理,是嵌入式开发中的首选设计;
  3. 单链表的优势是动态扩展、不依赖连续内存,劣势是不支持随机访问,查询效率为 O (n);
  4. 嵌入式开发中使用单链表,需重点关注内存泄漏线程安全问题,确保系统稳定运行。

作为嵌入式开发者,掌握单链表的实现是数据结构的入门基础,后续还可以深入学习双向链表、循环链表等复杂结构,为后续开发更复杂的嵌入式系统打下坚实基础。我是学嵌入式的小杨同学,关注我,一起解锁更多嵌入式 C 语言实战技巧!

相关推荐
CodeByV2 小时前
【算法题】链表
数据结构·算法
咋吃都不胖lyh2 小时前
RESTful API 调用详解(零基础友好版)
后端·restful
裴云飞2 小时前
Compose原理三之SlotTable
架构
源代码•宸2 小时前
Golang原理剖析(map)
经验分享·后端·算法·golang·哈希算法·散列表·map
wen__xvn2 小时前
代码随想录算法训练营DAY15第六章 二叉树part03
数据结构·算法·leetcode
Sagittarius_A*2 小时前
图像滤波:手撕五大经典滤波(均值 / 高斯 / 中值 / 双边 / 导向)【计算机视觉】
图像处理·python·opencv·算法·计算机视觉·均值算法
seeksky2 小时前
Transformer 注意力机制与序列建模基础
算法
冰暮流星2 小时前
c语言如何实现字符串复制替换
c语言·c++·算法
Swift社区2 小时前
LeetCode 374 猜数字大小 - Swift 题解
算法·leetcode·swift