ChongQu.html" title=环形缓冲区>环形缓冲区(Circular Buffer),也称为循环队列或Ring Buffer,是一种非常实用的数据结构,尤其在生产者-消费者模型中,用于解决数据传输速度不匹配的问题。本文将深入讲解C语言中ChongQu.html" title=环形缓冲区>环形缓冲区的实现原理、关键概念以及使用方法,并结合图解进行形象的解释。
1. 什么是ChongQu.html" title=环形缓冲区>环形缓冲区?
想象一个首尾相连的管道,数据从一端流入,从另一端流出。这就是ChongQu.html" title=环形缓冲区>环形缓冲区的基本模型。它使用一段固定大小的内存空间,通过读写指针的循环移动,实现高效的数据传递,避免了频繁的数据复制和移动。
2. 核心概念
ChongQu.html" title=环形缓冲区>环形缓冲区就像一个首尾相连的圆环,数据在这个环上循环流动。我们使用数组来模拟这个环,并用两个指针 start
(读指针)和 end
(写指针)来跟踪数据的读写位置
- 缓冲区大小(Length/Capacity): 缓冲区可以存储的最大数据量。
- 读指针(Start/Head): 指向下一个要读取的数据的位置。
- 写指针(End/Tail): 指向下一个要写入数据的位置。
- 空闲位置: 为了区分缓冲区“满”和“空”的状态,通常会保留一个空闲位置。这意味着缓冲区实际可用的存储空间比其物理大小少 1。这是非常重要的设计,避免了
start
和end
指针重合时,无法区分满和空的状态。
3. 图解ChongQu.html" title=环形缓冲区>环形缓冲区
假设我们有一个大小为 5 的数组来模拟ChongQu.html" title=环形缓冲区>环形缓冲区(实际有效存储空间为 4,需要保留一个空位)。
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
0 1 2 3 4 (数组索引)
^ ^
| |
start end
初始状态(空): start
和 end
指向同一个位置(通常是 0)。表示缓冲区没有任何数据。
+---+---+---+---+---+
| | | | | |
+---+---+---+---+---+
^ ^
| |
start end
首位相连,这其实end和start是重合的
写入数据: end
指针向后移动,并将数据写入到 end
指向的位置。
写入 'A'、'B'、'C':
+---+---+---+---+---+
| A | B | C | | |
+---+---+---+---+---+
^ ^
| |
start end
读取数据: start
指针向后移动,并读取 start
指向位置的数据。
读取 'A':
+---+---+---+---+---+
| | B | C | | |
+---+---+---+---+---+
^ ^
| |
start end
环绕: 当 end
指针到达数组末尾时,它会“绕回”到数组的开头。
写入 'D' 和 'E':
+---+---+---+---+---+
| E | B | C | D | |
+---+---+---+---+---+
^ ^
| |
start end
缓冲区满: 当 end
指针的下一个位置(环绕后)与 start
指针重合时,缓冲区就满了。此时不能再写入数据,除非先读取数据腾出空间。
如果继续写入,end
会追上 start
,导致数据覆盖。为了避免这种情况,我们保留一个空位,即当 (end + 1) % length == start
时,就认为缓冲区已满。
4. ChongQu.html" title=环形缓冲区>环形缓冲区基本功能代码说明
4.1 头文件函数、宏定义总览:
#ifndef _ring_buffer_h
#define _ring_buffer_h
typedef struct {
char *buffer; //
int length; // 缓冲区空间大小
int start; // 读指针位
int end; // 写指针位
} RingBuffer;
RingBuffer *RingBuffer_create(int length);
void RingBuffer_destroy(RingBuffer *buffer);
int RingBuffer_read(RingBuffer *buffer, char *target, int amount);
int RingBuffer_write(RingBuffer *buffer, char *data, int length);
void *RingBuffer_gets(RingBuffer *buffer);
#define RingBuffer_available_data(B) (((B)->end - (B)->start + (B)->length) % (B)->length)
#define RingBuffer_available_space(B) ((B)->length - RingBuffer_available_data(B) - 1)
#define RingBuffer_full(B) (RingBuffer_available_data((B)) + 1 == (B)->length)
#define RingBuffer_empty(B) (RingBuffer_available_data((B)) == 0)
#define RingBuffer_puts(B, D) RingBuffer_write((B), bdata((D)), blength((D)))
#define RingBuffer_get_all(B) RingBuffer_gets((B), RingBuffer_available_data((B)))
#define RingBuffer_starts_at(B) ((B)->buffer + (B)->start)
#define RingBuffer_ends_at(B) ((B)->buffer + (B)->end)
#define RingBuffer_commit_read(B, A) ((B)->start = ((B)->start + (A)) % (B)->length) //将读指针向前移动A个位置。
#define RingBuffer_commit_write(B, A) ((B)->end = ((B)->end + (A)) % (B)->length) // 将写指针向前移动A个位置。
#endif
4.1.1 RingBuffer
结构体
typedef struct {
char *buffer; // 存储数据的缓冲区
int length; // 缓冲区总长度(包含一个空闲位置)
int start; // 读指针
int end; // 写指针
} RingBuffer;
buffer
: 指向实际存储数据的内存区域。length
: 缓冲区的大小。为了区分空和满的状态,实际可用的存储空间是length - 1
。start
: 读指针,指向下一个要读取的数据的位置。end
: 写指针,指向下一个要写入数据的位置。
4.1.2 宏定义:
这些宏定义简化了对ChongQu.html" title=环形缓冲区>环形缓冲区的操作和状态判断。
RingBuffer_available_data(B)
: 计算缓冲区中已有的数据量。使用模运算%
实现环形效果。RingBuffer_available_space(B)
: 计算缓冲区中可用的空间量。注意要减去 1,因为要保留一个空位。RingBuffer_full(B)
: 判断缓冲区是否已满。RingBuffer_empty(B)
: 判断缓冲区是否为空。RingBuffer_starts_at(B)
: 获取start
指针指向的地址。RingBuffer_ends_at(B)
: 获取end
指针指向的地址。RingBuffer_commit_read(B, A)
: 将start
指针向前移动A
个位置。RingBuffer_commit_write(B, A)
: 将end
指针向前移动A
个位置。
4.2 .c文件中的函数实现
RingBuffer_create(int length)
: 创建ChongQu.html" title=环形缓冲区>环形缓冲区。
RingBuffer *RingBuffer_create(int length) {
RingBuffer *buffer = calloc(1, sizeof(RingBuffer)); // 分配 RingBuffer 结构体内存
check_mem(buffer); // 检查内存分配是否成功
buffer->length = length + 1; // 缓冲区实际长度比length大1
buffer->start = 0;
buffer->end = 0;
buffer->buffer = calloc(buffer->length, 1); // 分配缓冲区内存
check_mem(buffer->buffer); // 检查内存分配是否成功
return buffer;
error:
if (buffer) { // 内存分配失败时释放已分配的内存,防止内存泄漏
free(buffer);
}
return NULL;
}
RingBuffer_destroy(RingBuffer *buffer)
: 销毁ChongQu.html" title=环形缓冲区>环形缓冲区,释放内存。
void RingBuffer_destroy(RingBuffer *buffer) {
if (buffer) {
free(buffer->buffer); // 释放缓冲区内存
free(buffer); // 释放 RingBuffer 结构体内存
}
}
RingBuffer_write(RingBuffer *buffer, char *data, int length)
: 向缓冲区写入数据。
int RingBuffer_write(RingBuffer *buffer, char *data, int length) {
check(length > 0 && buffer != NULL && data != NULL, "Invalid inputs."); // 参数检查
check(length <= RingBuffer_available_space(buffer), // 检查是否有足够的空间
"Not enough space: %d request, %d available",
RingBuffer_available_data(buffer), length);
for (int i = 0; i < length; i++) {
*(RingBuffer_ends_at(buffer)) = *(data + i); // 写入数据
RingBuffer_commit_write(buffer, 1); // 移动 end 指针
}
return length; // 返回实际写入的字节数
error:
return -1; // 写入失败
}
RingBuffer_read(RingBuffer *buffer, char *target, int amount)
: 从缓冲区读取数据。
int RingBuffer_read(RingBuffer *buffer, char *target, int amount) {
check(amount > 0 && buffer != NULL && target != NULL, "Invalid inputs."); // 参数检查
check_debug(amount <= RingBuffer_available_data(buffer), // 检查是否有足够的数据
"Not enough in the buffer: has %d, needs %d",
RingBuffer_available_data(buffer), amount);
for (int i = 0; i < amount; i++) {
*(target + i) = *(RingBuffer_starts_at(buffer)); // 读取数据
RingBuffer_commit_read(buffer, 1); // 移动 start 指针
}
return amount; // 返回实际读取的字节数
error:
return -1; // 读取失败
}
RingBuffer_gets(RingBuffer *buffer)
: 获取缓冲区中的所有数据,但不修改缓冲区状态(即不移动 start
指针)。
void *RingBuffer_gets(RingBuffer *buffer) {
check(buffer != NULL, "Invalid inputs.");
char *result = NULL;
if (RingBuffer_empty(buffer)) { // 缓冲区为空
result = calloc(1, 1);
check_mem(result);
*result = '\0';
return result;
} else { // 缓冲区非空
int n = RingBuffer_available_data(buffer);
result = calloc(1, n + 1); // 分配 n+1 个字节,用于存储字符串和空终止符
check_mem(result);
int i = 0;
int current = buffer->start; // 保存 start 指针
while (i < n) {
result[i] = buffer->buffer[current];
current = (current + 1) % buffer->length; // 循环移动 current
i++;
}
result[n] = '\0'; // 添加字符串结束符
}
return result;
error:
if (result) free(result);
return NULL;
}
5. ChongQu.html" title=环形缓冲区>环形缓冲区的优势及其应用领域
应用场景:
- 生产者-消费者模型: 这是ChongQu.html" title=环形缓冲区>环形缓冲区最经典的应用场景。生产者向缓冲区写入数据,消费者从缓冲区读取数据,缓冲区起到了缓冲和解耦的作用。
- 网络数据传输: 在网络编程中,ChongQu.html" title=环形缓冲区>环形缓冲区常用于接收和发送数据,解决网络速度和应用程序处理速度不匹配的问题。
- 音频/视频处理: 音频和视频数据通常以流的形式传输,ChongQu.html" title=环形缓冲区>环形缓冲区可以有效地管理这些数据流。
- 中断处理: 在嵌入式系统中,中断处理程序通常会将数据写入ChongQu.html" title=环形缓冲区>环形缓冲区,然后由主程序进行处理。
相比于其他数据结构的优势:
- 高效的入队和出队操作: 只需要移动指针,不需要像普通队列那样移动大量数据。
- 避免频繁的内存分配和释放: 使用固定大小的内存空间,提高了性能。
- 适用于流式数据处理: 能够很好地处理连续不断的数据流。