循环缓冲区实现C语言

目录

介绍

循环缓冲区结构体

关键点解析

数据类型

循环缓冲区实现

循环缓冲区满和空的判断

满标志的使用

写入和读取的前置条件

满和空的判断条件

初始化循环缓冲区

反初始化循环缓冲区

判断循环缓冲区是否为空

判断循环缓冲区是否已满

获取循环缓冲区有效长度(字节为单位)

获取循环缓冲区空闲长度(字节为单位)

批量写入输出

批量读取数据

测试

测试输出


介绍

        循环缓冲区是一种数据结构,它使用一个固定大小的缓冲区,当缓冲区满了之后,新的数据会覆盖最旧的数据。它通常用于需要缓冲数据流的场景,比如生产者-消费者问题。在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 字节
剩余数据验证: 成功

你可能感兴趣的:(算法与数据结构,算法,c语言)