一、环形缓冲区的核心概念
环形缓冲区本质上是一个首尾相连的数组,通过两个关键指针实现数据的读写管理:
- 写指针(pw):标记下一个数据写入的位置
- 读指针(pr):标记下一个数据读取的位置
这两个指针是环形缓冲区的灵魂,所有的读写操作、状态判断都围绕它们展开。
二、读写指针的工作原理
1. 初始化状态
缓冲区为空时,写指针和读指针都指向数组的起始位置(下标0)。
2. 写入数据流程
- 第一次写入数据(如01)后,写指针pw右移一格
- 继续写入02、03……直到pw走到数组最后一格
- 此时继续写入新数据(如09),pw会自动跳回数组下标0,实现环形回绕,不会发生数组越界
- 再次写入10时,pw继续向后移动
3. 读取数据流程
- 读指针pr逐格向右移动,每读取一格,对应位置的数据清空
- pr读完06后,pw和pr同时移动,模拟日常环形缓冲区运行时的一般情况
- 当缓冲区内部所有数据全部读取完毕时,pw和pr会指向同一个下标位置
三、状态判断:空与满的判定
1. 空状态判断
当pw等于pr时,环形缓冲区为空。这是因为所有数据都已被读取完毕,两个指针重新重合。
2. 满状态判断
环形缓冲区采用“牺牲一格”方案,即数组中始终预留一个空白格子,用于区分空和满状态。
- 直观判断:pw加一等于pr时,缓冲区为满
- 边界情况处理:当pw在数组最后一位(下标7),pr在下标0时,单纯数字8不等于0,此时等式pw+1=pr不成立,但缓冲区实际上已经存满
- 通用公式:
(pw + 1) % buffer_size == pr,这是全场景通用无bug的判满条件
四、指针的实现代码
1. 写指针实现
// 方法一:先自增再取模 pw++; pw = pw % buffer_size; // 方法二:一行代码同时实现自增和取模 pw = (pw + 1) % buffer_size;2. 读指针实现
// 方法一:先自增再取模 pr++; pr = pr % buffer_size; // 方法二:一行代码同时实现自增和取模 pr = (pr + 1) % buffer_size;这两种写法实现的功能完全一致,都能保证指针在数组范围内循环移动。
五、完整的环形缓冲区实现
1. 头文件定义
#ifndef RING_BUFFER_H #define RING_BUFFER_H #include <stdint.h> #include <stdbool.h> #define BUFFER_SIZE 8 // 缓冲区大小,实际可用空间为BUFFER_SIZE - 1 typedef struct { uint8_t buffer[BUFFER_SIZE]; // 数据缓冲区 uint8_t pw; // 写指针 uint8_t pr; // 读指针 } RingBuffer_t; // 初始化环形缓冲区 void RingBuffer_Init(RingBuffer_t *rb); // 向环形缓冲区写入数据 bool RingBuffer_Write(RingBuffer_t *rb, uint8_t data); // 从环形缓冲区读取数据 bool RingBuffer_Read(RingBuffer_t *rb, uint8_t *data); // 判断环形缓冲区是否为空 bool RingBuffer_IsEmpty(RingBuffer_t *rb); // 判断环形缓冲区是否已满 bool RingBuffer_IsFull(RingBuffer_t *rb); #endif // RING_BUFFER_H2. 功能实现
#include "ring_buffer.h" // 初始化环形缓冲区 void RingBuffer_Init(RingBuffer_t *rb) { rb->pw = 0; rb->pr = 0; } // 向环形缓冲区写入数据 bool RingBuffer_Write(RingBuffer_t *rb, uint8_t data) { // 检查缓冲区是否已满 if (RingBuffer_IsFull(rb)) { return false; } // 写入数据 rb->buffer[rb->pw] = data; // 更新写指针 rb->pw = (rb->pw + 1) % BUFFER_SIZE; return true; } // 从环形缓冲区读取数据 bool RingBuffer_Read(RingBuffer_t *rb, uint8_t *data) { // 检查缓冲区是否为空 if (RingBuffer_IsEmpty(rb)) { return false; } // 读取数据 *data = rb->buffer[rb->pr]; // 更新读指针 rb->pr = (rb->pr + 1) % BUFFER_SIZE; return true; } // 判断环形缓冲区是否为空 bool RingBuffer_IsEmpty(RingBuffer_t *rb) { return rb->pw == rb->pr; } // 判断环形缓冲区是否已满 bool RingBuffer_IsFull(RingBuffer_t *rb) { return (rb->pw + 1) % BUFFER_SIZE == rb->pr; }3. 测试代码
#include <stdio.h> #include "ring_buffer.h" int main() { RingBuffer_t rb; uint8_t data; // 初始化环形缓冲区 RingBuffer_Init(&rb); // 写入数据 for (uint8_t i = 0; i < 7; i++) { if (RingBuffer_Write(&rb, i)) { printf("写入数据: %d\n", i); } else { printf("缓冲区已满,无法写入数据: %d\n", i); } } // 尝试写入第8个数据(应该失败) if (RingBuffer_Write(&rb, 7)) { printf("写入数据: 7\n"); } else { printf("缓冲区已满,无法写入数据: 7\n"); } // 读取数据 printf("\n读取数据:\n"); while (!RingBuffer_IsEmpty(&rb)) { if (RingBuffer_Read(&rb, &data)) { printf("读取数据: %d\n", data); } else { printf("缓冲区为空,无法读取数据\n"); } } // 尝试读取数据(应该失败) if (RingBuffer_Read(&rb, &data)) { printf("读取数据: %d\n", data); } else { printf("缓冲区为空,无法读取数据\n"); } return 0; }六、总结
环形缓冲区的核心优势在于读指针和写指针不会卡死在数组末尾,而是会自动循环回到头部,实现数据的连续读写。通过“牺牲一格”的方案,我们可以准确判断缓冲区的空和满状态,避免数据覆盖和丢失。