【嵌入式 C 语言实战】基于链表的队列(Queue)实现:FIFO 核心逻辑 + 完整代码解析

65 阅读9分钟

【嵌入式 C 语言实战】基于链表的队列(Queue)实现:FIFO 核心逻辑 + 完整代码解析

大家好,我是学嵌入式的小杨同学。在嵌入式开发中,队列(Queue)是遵循 “先进先出(FIFO)” 规则的核心数据结构,广泛应用于串口数据缓存、中断消息处理、任务间通信等场景。今天就基于你提供的代码,从队列的核心原理、代码逐行解析、问题修正到实战测试,手把手教你实现基于链表的队列,彻底掌握这一嵌入式必备技能。

一、队列的核心基础:为什么选择链表实现?

队列的实现方式主要有两种,各有优劣,嵌入式开发中需按需选择:

  • 数组实现:地址连续,访问速度快,但存在 “假溢出” 问题,扩容困难;
  • 链表实现:节点地址不连续,支持动态扩容,无内存浪费,是嵌入式场景的首选(尤其适合数据量不固定的场景)。

1. 队列的核心概念

  • 队头(front) :队列的起始位置,仅用于出队(删除数据);
  • 队尾(rear) :队列的末尾位置,仅用于入队(添加数据);
  • 空队列:队头指针与队尾指针指向同一位置(带头节点设计);
  • FIFO 规则:先入队的数据先出队,类似日常生活中的排队。

2. 队列的结构体定义(Queue.h)

在解析代码前,先补充Queue.h头文件的核心定义(你提供的代码依赖这些定义),这是链表队列的基础:

c

运行

#ifndef QUEUE_H
#define QUEUE_H

#include <stdio.h>
#include <stdlib.h>

// 队列数据类型定义(可根据需求修改,如char、float、结构体)
typedef int DATATYPE;

// 队列节点结构体(链表节点)
typedef struct QueueNode {
    DATATYPE data;          // 数据域:存储队列元素
    struct QueueNode *next; // 指针域:指向下一个节点
} QueueNode;

// 队列结构体(管理队头、队尾指针)
typedef struct Queue {
    QueueNode *front; // 队头指针(指向头节点)
    QueueNode *rear;  // 队尾指针(指向尾节点)
} Queue;

// 函数声明
Queue* InitQueue();          // 初始化队列
int IfEmpty(Queue *queue);   // 判断队列是否为空
int InQueue(Queue *queue, DATATYPE num); // 入队
int OUTQueue(Queue *queue);  // 出队
DATATYPE GetQueueHead(Queue* queue); // 获取队首元素
void PrintQueue(Queue *queue); // 打印队列
void DestroyQueue(Queue *queue); // 销毁队列(补充函数)

#endif

二、核心函数逐行解析:从初始化到出队

1. 初始化队列:InitQueue ()

初始化队列的核心是创建 “头节点”(不存储有效数据),并让队头、队尾指针都指向头节点,形成空队列。

c

运行

Queue* InitQueue()
{
    // 1. 为队列结构体分配内存
    Queue *queue=(Queue*)malloc(sizeof(Queue));
    if (queue == NULL) { // 补充判空:嵌入式必须检查内存分配结果
        printf("malloc error:队列结构体创建失败\n");
        return NULL;
    }
    // 2. 创建队列头节点(不存有效数据,简化边界处理)
    QueueNode *phead =(QueueNode*)malloc(sizeof(QueueNode));
    if (phead == NULL) {
        printf("malloc error:队列头节点创建失败\n");
        free(queue); // 释放已分配的队列结构体,避免内存泄漏
        return NULL;
    }
    phead->next=NULL; // 头节点初始无后续节点
    // 3. 队头、队尾指针都指向头节点(空队列状态)
    queue->front=queue->rear=phead;
    return queue;
}

关键要点

  • 头节点的作用是简化空队列判断(front == rear),避免单独处理 “队列为空” 的边界情况;
  • 必须检查malloc返回值,嵌入式设备内存有限,分配失败会导致程序崩溃。

2. 判断队列是否为空:IfEmpty ()

通过队头、队尾指针是否指向同一位置,判断队列是否为空,是所有队列操作的前置检查。

c

运行

int IfEmpty(Queue *queue)
{
    if (queue == NULL) { // 补充判空:避免传入空指针导致崩溃
        printf("队列指针为空,无法判断是否为空\n");
        return -1; // 返回-1表示异常,区别于1(空)、0(非空)
    }
    if(queue->front==queue->rear)
    {
        return 1; // 空队列返回1
    }
    return 0; // 非空返回0
}

优化说明:原代码未判断queue是否为NULL,若传入空指针会直接访问queue->front,导致程序崩溃,补充后更健壮。

3. 入队操作:InQueue ()

入队是向队尾添加数据,核心是 “创建新节点→链接到队尾→更新队尾指针”。

c

运行

int InQueue(Queue *queue,DATATYPE num)
{
    // 前置检查:队列是否初始化
    if (queue == NULL || queue->front == NULL || queue->rear == NULL) {
        printf("队列未初始化,无法入队\n");
        return 0; // 返回0表示入队失败
    }
    // 1. 创建新节点并填充数据
    QueueNode *newnode =(QueueNode*)malloc(sizeof(QueueNode));
    if (newnode == NULL) { // 检查内存分配
        printf("malloc error:入队节点创建失败\n");
        return 0;
    }
    newnode->data =num;
    newnode->next=NULL;
    // 2. 核心步骤1:原队尾节点的next指向新节点
    queue->rear->next=newnode;
    // 3. 核心步骤2:更新队尾指针,指向新节点
    queue->rear=newnode;
    return 1; // 返回1表示入队成功
}

逻辑图解

plaintext

空队列:front → 头节点 ← rear
入队10后:front → 头节点 → 10节点 ← rear
入队20后:front → 头节点 → 10节点 → 20节点 ← rear

4. 出队操作:OUTQueue ()

出队是从队头删除数据,核心是 “找到队首有效节点→取出数据→重新链接链表→释放节点→处理空队列”。

c

运行

int OUTQueue(Queue *queue)
{
    DATATYPE i=0;
    QueueNode *tempNode =NULL;
    // 前置检查:队列是否为空
    if (queue == NULL) {
        printf("队列指针为空,无法出队\n");
        return -1; // 异常返回-1
    }
    if(queue->rear==queue->front)
    {
        printf("队列为空\n");
        return 0;
    }
    // 1. 找到队首有效节点(头节点的下一个节点)
    tempNode = queue->front->next;
    // 2. 取出节点数据
    i=tempNode->data;
    // 3. 核心步骤:头节点指向队首节点的下一个节点
    queue->front->next=tempNode->next;
    // 4. 关键修正:判断是否为赋值(=)还是比较(==)
    if(tempNode->next == NULL) // 原代码是tempNode->next=NULL(赋值),会导致逻辑错误
    {
        queue->rear=queue->front; // 最后一个节点出队,恢复空队列状态
    }
    // 5. 释放出队节点的内存,避免泄漏
    free(tempNode);
    tempNode = NULL; // 置空指针,防止野指针
    return i; // 返回出队的元素
}

核心修正:原代码中if(tempNode->next=NULL)赋值操作,而非比较操作,会导致:

  • 无论tempNode->next是否为NULL,都会被赋值为NULL
  • 即使队列还有元素,queue->rear也会被重置为queue->front,导致后续入队、打印出错;修正为if(tempNode->next == NULL)(比较操作)后,仅当最后一个节点出队时,才重置队尾指针。

5. 获取队首元素:GetQueueHead ()

仅读取队首数据,不删除节点,是嵌入式中 “查看队列状态” 的常用操作。

c

运行

DATATYPE GetQueueHead(Queue* queue)
{
    // 前置检查
    if (queue == NULL) {
        printf("队列指针为空,无法获取队首元素\n");
        return -1; // 异常返回-1
    }
    if(queue->front == queue->rear)  
    {
        printf("队列为空\n");
        return 0;
    }
    return (queue->front->next->data); //返回队首元素(头节点的下一个节点)
}

6. 打印队列:PrintQueue ()

遍历队列并打印所有元素,用于验证队列操作的正确性,原代码逻辑完整,仅补充注释说明:

c

运行

void PrintQueue(Queue *queue)
{
    if (queue == NULL) 
    {
        printf("队列指针为空\n");
        return;
    }
    if (queue->front == queue->rear) 
    {
        printf("队列为空\n");
        return;
    }
    // 从队首有效节点开始遍历
    QueueNode *temp = queue->front->next; 
    printf("队列元素: ");
    while (temp != NULL) 
    {
        printf("%d\t", temp->data); 
        temp = temp->next;
    }
    printf("\n");
}

7. 补充函数:销毁队列(DestroyQueue)

嵌入式开发中,队列使用完毕后必须销毁,释放所有节点内存,避免内存泄漏:

c

运行

void DestroyQueue(Queue *queue)
{
    if (queue == NULL) return;
    QueueNode *temp = NULL;
    // 释放所有节点(包括头节点)
    while (queue->front != NULL) {
        temp = queue->front;
        queue->front = queue->front->next;
        free(temp);
        temp = NULL;
    }
    // 释放队列结构体
    free(queue);
    queue = NULL;
    printf("队列销毁成功\n");
}

三、完整实战:队列操作流程测试

将所有函数整合,编写测试代码,验证队列的 “初始化→入队→打印→出队→获取队首→销毁” 全流程:

c

运行

int main()
{
    // 1. 初始化队列
    Queue *queue = InitQueue();
    if (queue == NULL) {
        return -1;
    }
    printf("初始化队列后,队列是否为空:%d(1=空,0=非空)\n", IfEmpty(queue));

    // 2. 入队操作
    InQueue(queue, 10);
    InQueue(queue, 20);
    InQueue(queue, 30);
    printf("入队102030后:");
    PrintQueue(queue); // 输出:队列元素: 10    20    30

    // 3. 获取队首元素
    printf("队首元素:%d\n", GetQueueHead(queue)); // 输出:10

    // 4. 出队操作
    printf("出队元素:%d\n", OUTQueue(queue)); // 输出:10
    printf("出队后队列:");
    PrintQueue(queue); // 输出:队列元素: 20    30

    // 5. 全部出队
    OUTQueue(queue);
    OUTQueue(queue);
    printf("全部出队后,队列是否为空:%d\n", IfEmpty(queue)); // 输出:1

    // 6. 销毁队列
    DestroyQueue(queue);
    return 0;
}

编译与运行结果

bash

运行

# 编译命令
gcc queue.c -o queue
# 运行程序
./queue

plaintext

初始化队列后,队列是否为空:1(1=空,0=非空)
入队10、20、30后:队列元素: 10	20	30	
队首元素:10
出队元素:10
出队后队列:队列元素: 20	30	
全部出队后,队列是否为空:1
队列销毁成功

四、嵌入式开发关键注意事项

  1. 内存管理:嵌入式设备内存有限,malloc分配的节点必须用free释放(尤其是出队、销毁操作),避免内存泄漏;
  2. 指针安全:所有队列操作前必须检查queuefrontrear是否为NULL,防止野指针访问;
  3. 操作原子性:若在中断 / 多线程中操作队列(如中断入队、主线程出队),需添加互斥锁(如pthread_mutex_t)或关中断保护,避免并发访问导致队列混乱;
  4. 数据类型适配:修改DATATYPE的定义,可适配不同数据类型(如typedef char DATATYPE存储字符,typedef struct SensorData DATATYPE存储传感器数据);
  5. 避免赋值 / 比较混淆:出队函数中===的区别是嵌入式高频踩坑点,编写代码时需格外注意。

五、总结

基于链表的队列实现核心是 “链表的头尾指针管理”,关键要点可总结为:

  1. 带头节点设计:简化空队列判断(front == rear),避免边界处理复杂;
  2. 入队逻辑:队尾添加节点,更新rear指针;
  3. 出队逻辑:队头删除节点,更新front->next,最后一个节点出队时重置rear
  4. 内存安全:必须检查malloc返回值,使用完毕销毁队列,避免泄漏。

掌握队列的实现与使用,能轻松应对嵌入式开发中 “数据缓存”“消息传递” 等场景,比如串口接收数据时,用队列缓存数据,主线程从队列中读取并处理,避免数据丢失。

我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式数据结构实战技巧!