目录
介绍
循环缓冲区结构体
关键点解析
数据类型
循环缓冲区实现
循环缓冲区满和空的判断
满标志的使用
写入和读取的前置条件
满和空的判断条件
初始化循环缓冲区
反初始化循环缓冲区
判断循环缓冲区是否为空
判断循环缓冲区是否已满
获取循环缓冲区有效长度(字节为单位)
获取循环缓冲区空闲长度(字节为单位)
批量写入输出
批量读取数据
测试
测试输出
循环缓冲区是一种数据结构,它使用一个固定大小的缓冲区,当缓冲区满了之后,新的数据会覆盖最旧的数据。它通常用于需要缓冲数据流的场景,比如生产者-消费者问题。在C语言中,循环缓冲区可以通过一段内存和两个指针(或索引)来实现:一个指向读取位置,一个指向写入位置。
// 循环缓冲区结构体(字节缓冲区)
typedef struct {
uint8_t *buffer; // 缓冲区指针
uint32_t capacity; // 缓冲区总容量(字节数)
uint32_t read; // 下一个读取位置(字节偏移)
uint32_t write; // 下一个写入位置(字节偏移)
bool is_full; // 缓冲区满标志
} LoopBuffer;
uint8_t *buffer: buffer是循环缓冲区的指针,指向一片内存的起始位置。简单点,这段内存可以是一个数组,但通常是动态分配一段内存,比如使用malloc函数。
uint32_t capacity:表示循环缓冲区的大小,这里以字节为单位。即这个循环缓冲区是多少字节。
uint32_t read:读索引,指向下一个要读取的字节的位置
uint32_t write:写索引,指向下一个要写入的字节的位置
bool is_full:缓冲区满标志 用来判断缓冲区是否满
结构体中capacity、read、write的类型使用的是uint32_t,可以根据实际情况更改(实际能使用的缓冲区最大容量)。
循环缓冲区是如何实现的:分配的内存实际是一块顺序的,并不是环状的,是通过代码来实现的循环。这可以通过C语言中的取模运算符(%)实现。比如,每读取/写入一个字节,相应的读取/写入索引就自增1,达到capacicty就回到0。代码实现如下:
read = (read + 1) % capacity
write = (write + 1) % capacity
假设capacity = 5
则read = 位置0 1 2 3 4 只能是这5个数,write = 位置0 1 2 3 4也只能是这5个数。
仅仅通过write == read无法判断满和空,
比如初始状态:capacity = 5,read = 0,write = 0
此时read == write,没有写入数据,此时是空。
经过以下步骤:
初始状态:capacity = 5,read = 0,write = 0
写入第1个数到位置0之后:capacity = 5,read = 0,write = 1,write自增1变为1
写入第2个数到位置1之后:capacity = 5,read = 0,write = 2,write自增1变为2
写入第3个数到位置2之后:capacity = 5,read = 0,write = 3,write自增1变为3
写入第4个数到位置3之后:capacity = 5,read = 0,write = 4,write自增1变为4
写入第5个数到位置4之后:capacity = 5,read = 0,write = 0,write自增1变为0
此时,read == write,但是此时的缓冲区为满
bool is_full这个标志就用来区分满和空的状态,在写入和读取的函数中会判断并更新变量值。
每次写入N个字节(N > 0),write自增N后,记住是自增N后,判断write == read,如果成立,则is_full = true
每次读取N个字节(N > 0)后,is_full = false。因为读取后,循环缓冲区必定不可能是满的。
写入的前置条件是循环缓冲区不能是满,如果满,这里的处理方法是不写入;如果要写入的数据长度 > 空闲数据长度,则实际写入的数据长度最大只能是空闲数据长度。
读取的前置条件是循环缓冲区不能是空,如果空,这里的处理方法是不读取;如果要读取的数据长度 > 有效数据长度,则实际读取的数据长度最大只能是有效数据长度。
is_full == true 满
(!is_full == true) && (read == write) 空
/**
* brief: 初始化循环缓冲区
* param:capacity_bytes:循环缓冲区大小 以字节为单位
* return:LoopBuffer:返回初始化后的循环缓冲区指针
**/
LoopBuffer* loop_buf_init(uint32_t capacity_bytes)
{
LoopBuffer *lb = malloc(sizeof(LoopBuffer)); // malloc动态分配内存存储LoopBuffer结构体
if (!lb) return NULL; // 有效性判断 lb不为空,则表示分配成功
// 为空则返回NULL
lb->buffer = malloc(capacity_bytes); // malloc动态分配内存,指向循环缓冲区
if (!lb->buffer) // 有效性判断 不为空,则表示分配成功
{
free(lb); // 为空 则把之前分配成功的lb释放掉 防止内存碎片化
return NULL; // 为空 lb->buffer分配没成功,这个就不用释放了
}
lb->capacity = capacity_bytes; // 循环缓冲区容量赋值
lb->read = 0; // 读位置索引初始化为0
lb->write = 0; // 写位置所以初始化为0
lb->is_full = false; // 满标志初始化为false
return lb; // 返回分配成功的lp指针
}
/**
* brief:循环缓冲区反初始化 主要是释放之前动态分配的内存
* param:lb 指针
**/
void loop_buf_deinit(LoopBuffer *lb)
{
if (lb) // 指针有效性判断
{
free(lb->buffer); // 释放内存
free(lb); // 释放内存
}
}
/**
* brief:检查缓冲区是否为空
* param lb 循环缓冲区指针
* return ture:空 false:非空
**/
bool loop_buf_is_empty(const LoopBuffer *lb)
{
return (!lb->is_full && (lb->read == lb->write));
}
/**
* brief:检查缓冲区是否为满
* param lb 循环缓冲区指针
* return ture:满 false:非满
**/
bool loop_buf_is_full(const LoopBuffer *lb)
{
return lb->is_full;
}
/**
* brief 获取有效数据长度(已使用的字节数)
* param lb 循环缓冲区指针
* return 有效数据长度(以字节为单位)
**/
uint32_t loop_buf_size(const LoopBuffer *lb)
{
if (lb->is_full) return lb->capacity;
return (lb->write >= lb->read) ?
(lb->write - lb->read) :
(lb->capacity - lb->read + lb->write);
}
如果is_full为true,那么很容易得出结论(有效数据长度就是循环缓冲区总容量);如果不为满,则需要根据read和write的索引位置来讨论,其中就涉及到对"循环"一词的理解。
注意,这句话很重要:有效数据是从read位置开始数,到write结束,中间可能会涉及到"循环"(当write < read时),包括read位置,但不包括write位置。
1.is_full == true 循环缓冲区为满,则有效数据长度就是循环缓冲区总容量。
2.is_full != true && write >= read 则实际有效数据就是[read,write) 注意是前闭后开区间
举个例子,is_full == false,capacity = 5,read = 1,write = 3
则位置1,2上的是有效数据,共 3-1 = 2个。因为write指向的是下一个要写入的位置,表明该位置还没有被写入有效数据。注意区分某位置"存在数据"和"存在有效数据",我们不排除位置3"存在数据",因为之前可能经过好几轮的循环写入数据,但是按照我们循环缓冲区的涉及,该数据肯定已经被读取过了,不再是"有效数据"。
3.is_full != true && write < read 此时涉及到对循环的处理,实际有效数据是两段
第一段:[read,capacity) 前闭后开 有效数据数量 capacity - read
第二段: [0,write) 当write > 0时;有效数据数量 write - 0 = write
空集 当write == 0时;有效数据数量0 此时write也是0 也符合上面write > 0时候的公式,所以我们也可以用write表示。
综上分析:当is_full != true && write < read时,有效数据数量 = capacity - read + write
/**
* brief 获取空闲数据长度(未使用的字节数)
* param lb 循环缓冲区指针
* return 空闲数据长度(以字节为单位)
**/
uint32_t loop_buf_free_space(const LoopBuffer *lb)
{
return lb->capacity - loop_buf_size(lb);
}
注意,这句话很重要:空闲数据是从write位置开始数,到read结束,中间可能会涉及到"循环"(当write > read时),包括write位置,但不包括read位置。
当然也可以简便计算 总数据容量 = 有效数据长度 + 空闲数据长度
则空闲数据长度 = 总数据容量 - 有效数据长度
/**
* brief 批量写入数据
* param lb 循环指针 data:需要写入的数据的头指针 n_bytes:需要写入的数据的长度(以字节为单位)
* 返回值 实际写入的字节数
**/
uint32_t loop_buf_put(LoopBuffer *lb, const void *data, uint32_t n_bytes)
{
if (n_bytes == 0 || loop_buf_is_full(lb)) return 0;//写入数据长度为0或者已满则直接return
uint32_t free_space = loop_buf_free_space(lb);// 获取空闲数据长度 即能写入的最大数据长度
uint32_t to_write = (n_bytes > free_space) ? free_space : n_bytes;
// 判断得到实际写入的字节数
uint32_t written = 0; // 当前已经写的字节数 written == to_write才算真正写完
// 情况1:需要分两段处理(可能回绕)
if (lb->write >= lb->read)
{
// 第一段连续空间 [write,capacity)
uint32_t contig_space = lb->capacity - lb->write;// 第一段连续空间大小contig_space
uint32_t first_chunk = (to_write <= contig_space) ? to_write : contig_space;
// 判断得到在第一段连续空间实际写入的字节数first_chunk
// 写入第一段连续空间
memcpy(&lb->buffer[lb->write], data, first_chunk);// 字节copy
written += first_chunk; // 实际写入的字节数更新
lb->write = (lb->write + first_chunk) % lb->capacity;// 写入索引更新
// 第二段连续空间(如果需要回绕)
// first_chunk < to_write表示需要写入的数据在第一段连续空间没写完,则第二段继续写
if (first_chunk < to_write)
{
uint32_t second_chunk = to_write - first_chunk;
// 第二段连续空间需要写入的字节数second_chunk
memcpy(lb->buffer, (uint8_t*)data + first_chunk, second_chunk);
// 字节copy
written += second_chunk; // 实际写入的字节数更新
lb->write = second_chunk; // 写入索引更新
// 能进入这个条件说明lb->write == 0,而且加上second_chunk后也不会超过lb->capacity
// 所以这里代码可以简化,是没有问题的 当然按照下面的写法也没问题
//lb->write = (lb->write + second_chunk) % lb->capacity;
}
}
// 情况2:一段连续空间(不需要回绕)
else
{
// 第一段连续空间 [write,capacity)
uint32_t contig_space = lb->read - lb->write;// 第一段连续空间大小contig_space
uint32_t chunk = (to_write <= contig_space) ? to_write : contig_space;
// 判断得到在第一段连续空间实际写入的字节数chunk
// 写入连续空间
memcpy(&lb->buffer[lb->write], data, chunk);// 字节copy
written += chunk; // 实际写入的字节数更新
lb->write += chunk; // 写入索引更新
// 能进入这个条件说明lb->write加上chunk后不会超过lb->capacity
// 所以这里代码可以简化,是没有问题的 当然按照下面的写法也没问题
//lb->write = (lb->write + chunk) % lb->capacity;
}
// 更新满标志 这里加上written > 0 表示是实际写入了字节才更新标志
lb->is_full = (lb->write == lb->read && written > 0);
return written;
}
/**
* brief 批量读取数据
* param lb 循环指针 data:存放读取的数据的头指针 n_bytes:需要读取的数据的长度(以字节为单位)
* 返回值 实际读取的字节数
**/
uint32_t loop_buf_get(LoopBuffer *lb, void *data, uint32_t n_bytes)
{
if (n_bytes == 0 || loop_buf_is_empty(lb)) return 0;
//读取数据长度为0或者循环缓冲区为空则直接return
uint32_t data_size = loop_buf_size(lb);// 获取有效数据长度 即能读取的最大数据长度
uint32_t to_read = (n_bytes > data_size) ? data_size : n_bytes;
// 判断得到实际读取的字节数
uint32_t read = 0; // 当前已经读取的字节数 read == to_read 才算真正读完
// 情况1:连续数据足够(不需要回绕)
if (lb->read < lb->write) {
// 直接读取连续数据
memcpy(data, &lb->buffer[lb->read], to_read);// 字节copy
read += to_read; // 实际读取的字节数更新
lb->read += to_read; // 读取索引更新
// lb->read = (lb->read + to_read) % lb->capacity;
}
// 情况2:数据跨越缓冲区边界
else {
// 第一段连续空间 [read,capacity)
uint32_t contig_data = lb->capacity - lb->read;// 第一段连续空间大小contig_space
uint32_t first_chunk = (to_read <= contig_data) ? to_read : contig_data;
// 判断得到在第一段连续空间实际读取的字节数first_chunk
// 读取第一段连续空间数据
memcpy(data, &lb->buffer[lb->read], first_chunk);// 字节copy
read += first_chunk; // 实际读取的字节数更新
lb->read = (lb->read + first_chunk) % lb->capacity;// 读取索引更新
// 第二段连续空间(如果需要回绕)
// first_chunk < to_read表示第一段连续空间已读完但没达到要读取的字节数,第二段继续读
if (first_chunk < to_read) {
uint32_t second_chunk = to_read - first_chunk;
// 第二段连续空间需要读取的字节数second_chunk
memcpy((uint8_t*)data + first_chunk, lb->buffer, second_chunk);
// 字节copy
read += second_chunk; // 实际读取的字节数更新
lb->read = second_chunk; // 读取索引更新
// lb->read = (lb->read + to_read) % lb->capacity;
}
}
// 读取后缓冲区不可能满
lb->is_full = false;
return read;
}
// 打印缓冲区状态(十六进制格式)
void loop_buf_print_hex(const LoopBuffer *lb) {
printf("Buffer [");
uint32_t count = loop_buf_size(lb);
uint32_t index = lb->read;
for (uint32_t i = 0; i < count; i++) {
printf("%02X", lb->buffer[index]);
if (i < count - 1) printf(" ");
index = (index + 1) % lb->capacity;
}
printf("] (Bytes: %u/%u, Free: %u)\n",
loop_buf_size(lb), lb->capacity,
loop_buf_free_space(lb));
}
// 测试大容量缓冲区
void test_large_buffer() {
printf("\n===== 大容量缓冲区测试 =====\n");
const uint32_t capacity = 10000; // 10KB缓冲区
LoopBuffer *buf = loop_buf_init(capacity);
// 写入数据
uint8_t *data = malloc(capacity);
for (uint32_t i = 0; i < capacity; i++) {
data[i] = i % 256;
}
uint32_t written = loop_buf_put(buf, data, capacity);
printf("写入 %u 字节 (容量 %u)\n", written, capacity);
// 读取前半部分
uint32_t half = capacity / 2;
uint8_t *read_buf = malloc(half);
uint32_t read = loop_buf_get(buf, read_buf, half);
printf("读取 %u 字节\n", read);
// 验证数据
bool valid = true;
for (uint32_t i = 0; i < half; i++) {
if (read_buf[i] != (i % 256)) {
valid = false;
break;
}
}
printf("前半部分数据验证: %s\n", valid ? "成功" : "失败");
// 写入更多数据
written = loop_buf_put(buf, data, half);
printf("再写入 %u 字节\n", written);
// 读取剩余数据
uint32_t remaining = capacity - half + half;
uint8_t *read_buf2 = malloc(remaining);
read = loop_buf_get(buf, read_buf2, remaining);
printf("读取 %u 字节\n", read);
// 验证剩余数据
valid = true;
for (uint32_t i = 0; i < half; i++) { // 后半部分原始数据
if (read_buf2[i] != ((i + half) % 256)) {
valid = false;
break;
}
}
for (uint32_t i = 0; i < half; i++) { // 新写入的数据
if (read_buf2[half + i] != (i % 256)) {
valid = false;
break;
}
}
printf("剩余数据验证: %s\n", valid ? "成功" : "失败");
free(data);
free(read_buf);
free(read_buf2);
loop_buf_deinit(buf);
}
int main() {
// 创建10字节缓冲区
LoopBuffer *buf = loop_buf_init(10);
printf("===== 基本功能测试 =====\n");
printf("初始化: ");
loop_buf_print_hex(buf);
// 测试基本写入
uint8_t data1[] = {0x11, 0x22, 0x33, 0x44};
uint32_t written = loop_buf_put(buf, data1, sizeof(data1));
printf("写入%u字节: ", written);
loop_buf_print_hex(buf);
// 测试边界回绕
uint8_t data2[] = {0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB};
written = loop_buf_put(buf, data2, sizeof(data2));
printf("写入%u字节: ", written);
loop_buf_print_hex(buf);
// 测试读取
uint8_t read_buf[10];
uint32_t read = loop_buf_get(buf, read_buf, 5);
printf("读取%u字节: ", read);
for (uint32_t i = 0; i < read; i++) printf("%02X ", read_buf[i]);
printf("\n缓冲区状态: ");
loop_buf_print_hex(buf);
// 测试小数据写入
uint8_t data3[] = {0xCC, 0xDD};
written = loop_buf_put(buf, data3, sizeof(data3));
printf("写入%u字节后: ", written);
loop_buf_print_hex(buf);
// 测试读取所有数据
read = loop_buf_get(buf, read_buf, loop_buf_size(buf));
printf("读取%u字节: ", read);
for (uint32_t i = 0; i < read; i++) printf("%02X ", read_buf[i]);
printf("\n最终状态: ");
loop_buf_print_hex(buf);
loop_buf_deinit(buf);
// 运行大容量测试
test_large_buffer();
return 0;
}
===== 基本功能测试 =====
初始化: Buffer [] (Bytes: 0/10, Free: 10)
写入4字节: Buffer [11223344] (Bytes: 4/10, Free: 6)
写入6字节: Buffer [112233445566778899AA] (Bytes: 10/10, Free: 0)
读取5字节: 11 22 33 44 55
缓冲区状态: Buffer [66778899AA] (Bytes: 5/10, Free: 5)
写入2字节后: Buffer [66778899AACCDD] (Bytes: 7/10, Free: 3)
读取7字节: 66 77 88 99 AA CC DD
最终状态: Buffer [] (Bytes: 0/10, Free: 10)
===== 大容量缓冲区测试 =====
写入 10000 字节 (容量 10000)
读取 5000 字节
前半部分数据验证: 成功
再写入 5000 字节
读取 10000 字节
剩余数据验证: 成功