【嵌入式 C 语言实战】栈、队列、二叉树核心解析:存储原理 + 应用场景 + 实现思路
大家好,我是学嵌入式的小杨同学。在嵌入式开发中,栈、队列、二叉树是除了链表之外最核心的三种数据结构,它们各自有着独特的存储规则和应用场景 ------ 栈的 "先进后出"、队列的 "先进先出"、二叉树的 "层级存储",分别对应中断机制、消息传递、资源管理等高频需求。今天就结合资料,系统讲解这三种数据结构的核心原理、存储方式和嵌入式实战场景,帮你一次性掌握嵌入式数据结构的 "半壁江山"。
一、先理清核心:三种数据结构的本质区别
栈、队列、二叉树虽同属数据结构,但存储规则和核心用途完全不同,用一张表就能快速区分:
| 数据结构 | 核心特点 | 存储规则 | 生活类比 | 嵌入式核心场景 |
|---|---|---|---|---|
| 栈(Stack) | 先进后出(FILO) | 仅从一端(栈顶)插入 / 删除数据 | 有底的盒子(先放的后拿) | 函数调用栈、中断机制、局部变量存储 |
| 队列(Queue) | 先进先出(FIFO) | 从一端(队尾)插入、另一端(队首)删除 | 排队(先到先得) | 消息队列(MQTT)、串口数据缓存、任务调度 |
| 二叉树(Binary Tree) | 层级结构 | 每个节点最多两个子节点(左 / 右子树) | 族谱、公司组织结构 | 资源管理器、设备树解析、语法解析树 |
二、栈(Stack):先进后出的 "数据盒子"
栈是嵌入式开发中最基础也最常用的数据结构,其核心是 "只能在栈顶操作数据",操作效率极高(时间复杂度 O (1))。
1. 栈的核心概念
- 栈底(pstart):栈的起始位置,固定不变;
- 栈顶(ptemp):永远指向下一个可存储数据的位置,随数据插入 / 删除移动;
- 栈空:栈顶指针(ptemp)与栈底指针(pstart)重合;
- 栈满:栈顶指针(ptemp)超过栈空间的末尾(pend+1)。
2. 两种存储方式(嵌入式实战)
(1)数组存储(线性存储)
用固定大小的数组作为栈空间,实现简单、访问速度快,适合数据量固定的场景(如小型缓存)。
c
运行
#include <stdio.h>
#include <stdlib.h>
#define STACK_SIZE 10 // 栈空间大小
// 栈结构体定义
typedef struct {
int data[STACK_SIZE]; // 栈空间(数组)
int *pstart; // 栈底指针
int *pend; // 栈空间末尾指针
int *ptemp; // 栈顶指针
} Stack;
// 初始化栈
Stack* InitStack() {
Stack *stack = (Stack *)malloc(sizeof(Stack));
if (stack == NULL) {
printf("malloc error:栈初始化失败\n");
return NULL;
}
stack->pstart = stack->data; // 栈底指向数组起始位置
stack->pend = stack->data + STACK_SIZE - 1; // 栈尾指向数组最后一个元素
stack->ptemp = stack->pstart; // 栈顶初始指向栈底(空栈)
return stack;
}
// 入栈(压栈)
int PushStack(Stack *stack, int num) {
// 判断栈满
if (stack->ptemp > stack->pend) {
printf("栈满,无法入栈\n");
return 0;
}
*stack->ptemp = num; // 数据存入栈顶
stack->ptemp++; // 栈顶指针上移
return 1;
}
// 出栈(弹栈)
int PopStack(Stack *stack) {
// 判断栈空
if (stack->ptemp == stack->pstart) {
printf("栈空,无法出栈\n");
return -1;
}
stack->ptemp--; // 栈顶指针下移
return *stack->ptemp; // 返回栈顶数据
}
// 打印栈
void PrintStack(Stack *stack) {
if (stack->ptemp == stack->pstart) {
printf("栈空\n");
return;
}
int *temp = stack->pstart;
printf("栈元素(从栈底到栈顶):");
while (temp < stack->ptemp) {
printf("%d ", *temp);
temp++;
}
printf("\n");
}
(2)链表存储(链式存储)
用链表的头插法实现栈,支持动态扩容,无栈满限制,适合数据量不固定的场景。核心逻辑:
- 入栈:头插法新增节点(栈顶为链表头部);
- 出栈:删除链表头部节点(栈顶节点);
- 优势:无需预先分配固定空间,内存利用率更高。
3. 嵌入式核心应用场景
- 函数调用栈:C 语言函数调用时,参数、返回地址、局部变量会自动压入栈中,函数执行完毕后弹栈;
- 中断机制:中断发生时,CPU 会将当前程序状态压入栈,中断处理完成后弹栈恢复;
- 临时数据缓存:如串口接收短数据时,用栈临时存储,处理完毕后依次弹栈。
三、队列(Queue):先进先出的 "数据管道"
队列与栈相反,遵循 "先进先出" 规则,核心是 "两端操作",是嵌入式中数据传递的核心载体。
1. 队列的核心概念
- 队空间:存储数据的内存区域(数组或链表);
- 队底(pstart):队列的起始位置;
- 队顶(pend):队列的末尾位置;
- 入队指针(pin):指向下一个可入队的位置;
- 出队指针(pout):指向下一个可出队的位置;
- 队空:计数变量 count=0;
- 队满:计数变量 count=total(允许入队的总数量)。
2. 两种存储方式(嵌入式实战)
(1)数组存储(循环队列)
用数组实现循环队列,解决 "假溢出" 问题(数组尾部满但头部有空位),是嵌入式高频实现方式。核心逻辑:
- 入队:pin 指向的位置存入数据,pin++;若 pin 超过 pend,重置为 pstart;
- 出队:取出 pout 指向的数据,pout++;若 pout 超过 pend,重置为 pstart;
- 用 count 计数,避免入队 / 出队指针重合时无法判断空满。
c
运行
#define QUEUE_TOTAL 10 // 队列最大容量
// 循环队列结构体
typedef struct {
int data[QUEUE_TOTAL]; // 队空间(数组)
int *pstart; // 队底指针
int *pend; // 队顶指针
int pin; // 入队指针(数组下标)
int pout; // 出队指针(数组下标)
int count; // 当前队列元素个数
} CircleQueue;
// 初始化循环队列
CircleQueue* InitCircleQueue() {
CircleQueue *queue = (CircleQueue *)malloc(sizeof(CircleQueue));
if (queue == NULL) {
printf("malloc error:队列初始化失败\n");
return NULL;
}
queue->pstart = queue->data;
queue->pend = queue->data + QUEUE_TOTAL - 1;
queue->pin = 0;
queue->pout = 0;
queue->count = 0;
return queue;
}
// 入队
int InQueue(CircleQueue *queue, int num) {
if (queue->count == QUEUE_TOTAL) {
printf("队满,无法入队\n");
return 0;
}
queue->data[queue->pin] = num;
queue->pin++;
// 循环:pin超过队尾,重置为队头
if (queue->pin > QUEUE_TOTAL - 1) {
queue->pin = 0;
}
queue->count++;
return 1;
}
// 出队
int OutQueue(CircleQueue *queue) {
if (queue->count == 0) {
printf("队空,无法出队\n");
return -1;
}
int data = queue->data[queue->pout];
queue->pout++;
// 循环:pout超过队尾,重置为队头
if (queue->pout > QUEUE_TOTAL - 1) {
queue->pout = 0;
}
queue->count--;
return data;
}
(2)链表存储(链式队列)
用链表的尾插法实现队列,动态扩容,无队满限制,适合大数据量场景(如消息队列)。核心逻辑:
- 入队:尾插法新增节点(队尾为链表尾部);
- 出队:删除链表头部节点(队首节点);
- 优势:无需担心队列溢出,适合数据量波动大的场景。
3. 嵌入式核心应用场景
- 消息队列(MQTT):物联网设备中,传感器数据、控制指令通过队列缓存,按顺序处理;
- 串口数据缓存:串口接收数据时,用队列异步缓存,主线程同步读取处理,避免数据丢失;
- 任务调度:RTOS 中,就绪队列存储等待执行的任务,调度器按优先级依次取出任务执行。
四、二叉树(Binary Tree):层级分明的 "数据结构树"
二叉树是树形结构的基础,每个节点最多有两个子节点(左子树、右子树),适合层级化数据存储和查询。
1. 二叉树的核心概念
- 根节点:树的顶层节点,无父节点;
- 子节点:每个节点的左、右子节点,构成左子树、右子树;
- 叶子节点:无左、右子节点的节点;
- 层级:从根节点开始计数,根节点为第 1 层。
2. 存储方式(链式存储,嵌入式首选)
用结构体定义节点,包含数据域和左 / 右子节点指针,是二叉树的标准实现方式。
c
运行
// 二叉树节点结构体
typedef struct TreeNode {
int data; // 数据域
struct TreeNode *left; // 左子节点指针
struct TreeNode *right; // 右子节点指针
} TreeNode;
// 创建二叉树节点
TreeNode* CreateTreeNode(int data) {
TreeNode *node = (TreeNode *)malloc(sizeof(TreeNode));
if (node == NULL) {
printf("malloc error:节点创建失败\n");
return NULL;
}
node->data = data;
node->left = NULL;
node->right = NULL;
return node;
}
// 构建示例二叉树(手动构建)
TreeNode* BuildBinaryTree() {
// 根节点
TreeNode *root = CreateTreeNode(1);
// 第二层节点
root->left = CreateTreeNode(2);
root->right = CreateTreeNode(3);
// 第三层节点
root->left->left = CreateTreeNode(4);
root->left->right = CreateTreeNode(5);
return root;
}
// 前序遍历(根→左→右)
void PreOrderTraversal(TreeNode *root) {
if (root != NULL) {
printf("%d ", root->data);
PreOrderTraversal(root->left);
PreOrderTraversal(root->right);
}
}
3. 嵌入式核心应用场景
- 资源管理器:嵌入式系统的文件系统(如 FAT32),本质是二叉树的变种(多叉树),用于层级管理文件和目录;
- 设备树解析:ARM 嵌入式设备中,设备树(Device Tree)用树形结构描述硬件配置,内核启动时解析设备树;
- 语法解析树:编译器编译代码时,会将源代码解析为二叉树结构,用于语法检查和指令生成。
五、嵌入式开发关键注意事项
- 存储方式选择 :
- 数据量固定、追求速度:选数组存储(栈 / 队列);
- 数据量不固定、避免浪费:选链表存储(栈 / 队列 / 二叉树);
- 内存管理 :
- 链式存储时,
malloc分配的节点必须用free释放,避免内存泄漏; - 数组存储时,需合理设置空间大小,避免溢出或浪费;
- 链式存储时,
- 线程安全 :
- 多线程 / 中断中操作栈 / 队列,需添加互斥锁或关中断保护,避免并发访问导致数据混乱;
- 效率优化 :
- 栈 / 队列的操作效率均为 O (1),二叉树遍历效率为 O (n),大数据量查询建议用二叉搜索树(BST)优化。
六、总结
栈、队列、二叉树是嵌入式 C 语言的核心数据结构,核心要点可总结为:
- 栈:先进后出,适合临时存储、函数调用、中断处理;
- 队列:先进先出,适合数据传递、消息缓存、任务调度;
- 二叉树:层级结构,适合层级化数据存储、资源管理、解析场景;
- 存储方式:数组快但固定,链表灵活但需管理内存,嵌入式需按需选择。
掌握这三种数据结构,能轻松应对嵌入式开发中 80% 以上的复杂数据处理场景,为后续学习 RTOS、文件系统、设备驱动打下坚实基础。我是学嵌入式的小杨同学,关注我,后续会分享更多嵌入式实战技巧!