环形缓冲区,也称为循环队列或循环缓冲区,是一种先进先出的数据结构,它用一块固定的连续内存空间来存储数据,它的特点是“头尾相接”,就像一个环,数据可以不断地写入和读取,而无需移动内存中的数据。

(图片来源网络,侵删)
为什么使用环形缓冲区?
- 高效:相比于标准队列(需要频繁的
memmove或malloc/free),环形缓冲区的读写操作通常是 O(1) 的时间复杂度,非常高效。 - 内存友好:大小固定,避免了动态内存分配的开销和碎片化问题。
- 应用广泛:非常适合生产者-消费者模型,
- 嵌入式系统:UART 串口通信、ADC/DAC 数据采集。
- 操作系统:进程间通信、设备驱动。
- 网络编程:数据包的缓存和处理。
- 音频/视频流处理:缓冲音视频数据。
环形缓冲区的基本原理
想象一个固定大小的数组,它有两个“指针”:
- 写入指针:指向下一个数据将要写入的位置。
- 读取指针:指向下一个将要读取的数据的位置。
这两个指针在数组中循环移动。
核心状态判断
判断缓冲区是“空”还是“满”是环形缓冲区实现的关键,最简单的方法是使用一个额外的 size 变量来记录当前数据量,但更巧妙、更常用的方法是利用指针的位置关系,而无需额外的变量。
- 缓冲区为空:当
读指针 == 写指针时,缓冲区为空。 - 缓冲区为满:当
(写指针 + 1) % 缓冲区大小 == 读指针时,缓冲区为满,这里我们保留一个空位,是为了区分“空”和“满”的状态,避免了使用额外的标志位。
C 语言实现
下面是一个完整、可运行的 C 语言环形缓冲区实现,包含了结构体定义、初始化、写入、读取、状态查询等核心功能。

(图片来源网络,侵删)
定义结构体
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
// 定义一个通用的环形缓冲区结构体
typedef struct {
uint8_t *buffer; // 存储数据的数组
size_t size; // 缓冲区的总容量
size_t head; // 写入指针
size_t tail; // 读取指针
} RingBuffer;
// 为了方便,我们使用一个宏来计算下一个位置
#define NEXT_POS(pos, size) ((pos + 1) % (size))
初始化函数
/**
* @brief 初始化环形缓冲区
* @param rb 环形缓冲区指针
* @param buffer 外部提供的存储数组
* @param size 缓冲区大小
*/
void RingBuffer_Init(RingBuffer *rb, uint8_t *buffer, size_t size) {
rb->buffer = buffer;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
状态查询函数
/**
* @brief 判断缓冲区是否为空
* @param rb 环形缓冲区指针
* @return true 为空, false 不为空
*/
bool RingBuffer_IsEmpty(const RingBuffer *rb) {
return rb->head == rb->tail;
}
/**
* @brief 判断缓冲区是否为满
* @param rb 环形缓冲区指针
* @return true 为满, false 不为满
*/
bool RingBuffer_IsFull(const RingBuffer *rb) {
return NEXT_POS(rb->head, rb->size) == rb->tail;
}
/**
* @brief 获取缓冲区中当前数据量
* @param rb 环形缓冲区指针
* @return 当前数据量
*/
size_t RingBuffer_GetCount(const RingBuffer *rb) {
if (rb->head >= rb->tail) {
return rb->head - rb->tail;
} else {
return rb->size - (rb->tail - rb->head);
}
}
写入数据函数
/**
* @brief 向环形缓冲区写入一个字节
* @param rb 环形缓冲区指针
* @param data 要写入的数据
* @return true 写入成功, false 缓冲区已满
*/
bool RingBuffer_Put(RingBuffer *rb, uint8_t data) {
if (RingBuffer_IsFull(rb)) {
return false; // 缓冲区满,无法写入
}
rb->buffer[rb->head] = data;
rb->head = NEXT_POS(rb->head, rb->size);
return true;
}
/**
* @brief 向环形缓冲区写入一块数据
* @param rb 环形缓冲区指针
* @param data 数据块指针
* @param len 数据长度
* @return 实际写入的字节数
*/
size_t RingBuffer_PutBlock(RingBuffer *rb, const uint8_t *data, size_t len) {
size_t written = 0;
// 检查剩余空间
size_t available = rb->size - RingBuffer_GetCount(rb) - 1; // -1 for the "full" slot
if (len > available) {
len = available; // 只写入能容纳的部分
}
// 分两段写入,以防数据跨越缓冲区末尾
size_t first_chunk = rb->size - rb->head;
if (len < first_chunk) {
first_chunk = len;
}
// 写入第一段
memcpy(&rb->buffer[rb->head], data, first_chunk);
rb->head = (rb->head + first_chunk) % rb->size;
written += first_chunk;
// 如果还有剩余,写入第二段
if (written < len) {
memcpy(&rb->buffer[rb->head], data + written, len - written);
rb->head = (rb->head + len - written) % rb->size;
written += (len - written);
}
return written;
}
读取数据函数
/**
* @brief 从环形缓冲区读取一个字节
* @param rb 环形缓冲区指针
* @param data 用于存储读取数据的指针
* @return true 读取成功, false 缓冲区为空
*/
bool RingBuffer_Get(RingBuffer *rb, uint8_t *data) {
if (RingBuffer_IsEmpty(rb)) {
return false; // 缓冲区空,无法读取
}
*data = rb->buffer[rb->tail];
rb->tail = NEXT_POS(rb->tail, rb->size);
return true;
}
/**
* @brief 从环形缓冲区读取一块数据
* @param rb 环形缓冲区指针
* @param data 用于存储读取数据的缓冲区
* @param len 想要读取的长度
* @return 实际读取的字节数
*/
size_t RingBuffer_GetBlock(RingBuffer *rb, uint8_t *data, size_t len) {
size_t count = RingBuffer_GetCount(rb);
if (len > count) {
len = count; // 只读取存在的数据
}
// 分两段读取,以防数据跨越缓冲区末尾
size_t first_chunk = rb->size - rb->tail;
if (len < first_chunk) {
first_chunk = len;
}
// 读取第一段
memcpy(data, &rb->buffer[rb->tail], first_chunk);
rb->tail = (rb->tail + first_chunk) % rb->size;
size_t read = first_chunk;
// 如果还有剩余,读取第二段
if (read < len) {
memcpy(data + read, &rb->buffer[rb->tail], len - read);
rb->tail = (rb->tail + len - read) % rb->size;
read += (len - read);
}
return read;
}
完整示例与测试
下面是一个 main 函数,演示如何使用上述的环形缓冲区。
int main() {
// 1. 定义一个足够大的数组作为缓冲区
const size_t BUFFER_SIZE = 16;
uint8_t buffer_data[BUFFER_SIZE];
// 2. 创建并初始化环形缓冲区
RingBuffer rb;
RingBuffer_Init(&rb, buffer_data, BUFFER_SIZE);
printf("Buffer size: %zu\n", BUFFER_SIZE);
printf("Initial state: Empty=%s, Full=%s\n",
RingBuffer_IsEmpty(&rb) ? "true" : "false",
RingBuffer_IsFull(&rb) ? "true" : "false");
// 3. 写入数据
printf("\n--- Writing data (A, B, C, D) ---\n");
RingBuffer_Put(&rb, 'A');
RingBuffer_Put(&rb, 'B');
RingBuffer_Put(&rb, 'C');
RingBuffer_Put(&rb, 'D');
printf("Current count: %zu\n", RingBuffer_GetCount(&rb));
printf("State: Empty=%s, Full=%s\n",
RingBuffer_IsEmpty(&rb) ? "true" : "false",
RingBuffer_IsFull(&rb) ? "true" : "false");
// 4. 读取数据
printf("\n--- Reading data ---\n");
uint8_t ch;
while (RingBuffer_Get(&rb, &ch)) {
printf("Read: %c\n", ch);
}
printf("State after reading: Empty=%s, Full=%s\n",
RingBuffer_IsEmpty(&rb) ? "true" : "false",
RingBuffer_IsFull(&rb) ? "true" : "false");
// 5. 测试块写入和读取
printf("\n--- Testing block operations ---\n");
uint8_t write_block[] = "Hello, RingBuffer!";
size_t written = RingBuffer_PutBlock(&rb, write_block, strlen(write_block));
printf("Wrote %zu bytes\n", written);
printf("Current count: %zu\n", RingBuffer_GetCount(&rb));
uint8_t read_block[32];
size_t read = RingBuffer_GetBlock(&rb, read_block, sizeof(read_block));
read_block[read] = '\0'; // 确保字符串以 null
printf("Read %zu bytes: %s\n", read, read_block);
// 6. 测试缓冲区满的情况
printf("\n--- Testing buffer full condition ---\n");
// 填满缓冲区
for (int i = 0; i < BUFFER_SIZE - 1; ++i) {
RingBuffer_Put(&rb, 'X');
}
printf("Current count: %zu\n", RingBuffer_GetCount(&rb));
printf("Is full? %s\n", RingBuffer_IsFull(&rb) ? "true" : "false");
// 尝试再写入一个字节
if (!RingBuffer_Put(&rb, 'Y')) {
printf("Buffer is full, cannot write 'Y'.\n");
}
return 0;
}
编译和运行
将以上所有代码保存为 ringbuffer_example.c,然后使用 GCC 编译:
gcc ringbuffer_example.c -o ringbuffer_example
运行:
./ringbuffer_example
预期输出
Buffer size: 16
Initial state: Empty=true, Full=false
--- Writing data (A, B, C, D) ---
Current count: 4
State: Empty=false, Full=false
--- Reading data ---
Read: A
Read: B
Read: C
Read: D
State after reading: Empty=true, Full=false
--- Testing block operations ---
Wrote 18 bytes
Current count: 18
Read 18 bytes: Hello, RingBuffer!
--- Testing buffer full condition ---
Current count: 15
Is full? true
Buffer is full, cannot write 'Y'.
总结与注意事项
- 线程安全:上述实现是非线程安全的,如果在多线程环境中使用(一个线程写,一个线程读),你必须使用互斥锁 来保护对
RingBuffer结构体的所有访问,以防止竞争条件。 - 数据类型:示例中使用
uint8_t,你可以根据需要修改为int,char或其他任何数据类型,如果存储结构体,确保memcpy的使用是安全的(结构体中不包含指针)。 - “满”的判断:保留一个空位来判断缓冲区是否已满是常见的做法,如果你不介意使用一个额外的
bool标志位来记录状态,也可以实现一个“写指针追上读指针”即为满的版本,但这会增加实现的复杂性。 - 灵活性:这个实现非常灵活,可以轻松地集成到任何项目中。

(图片来源网络,侵删)
