【嵌入式 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("入队10、20、30后:");
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
队列销毁成功
四、嵌入式开发关键注意事项
- 内存管理:嵌入式设备内存有限,
malloc分配的节点必须用free释放(尤其是出队、销毁操作),避免内存泄漏; - 指针安全:所有队列操作前必须检查
queue、front、rear是否为NULL,防止野指针访问; - 操作原子性:若在中断 / 多线程中操作队列(如中断入队、主线程出队),需添加互斥锁(如
pthread_mutex_t)或关中断保护,避免并发访问导致队列混乱; - 数据类型适配:修改
DATATYPE的定义,可适配不同数据类型(如typedef char DATATYPE存储字符,typedef struct SensorData DATATYPE存储传感器数据); - 避免赋值 / 比较混淆:出队函数中
==和=的区别是嵌入式高频踩坑点,编写代码时需格外注意。
五、总结
基于链表的队列实现核心是 “链表的头尾指针管理”,关键要点可总结为:
- 带头节点设计:简化空队列判断(
front == rear),避免边界处理复杂; - 入队逻辑:队尾添加节点,更新
rear指针; - 出队逻辑:队头删除节点,更新
front->next,最后一个节点出队时重置rear; - 内存安全:必须检查
malloc返回值,使用完毕销毁队列,避免泄漏。
掌握队列的实现与使用,能轻松应对嵌入式开发中 “数据缓存”“消息传递” 等场景,比如串口接收数据时,用队列缓存数据,主线程从队列中读取并处理,避免数据丢失。
我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式数据结构实战技巧!