C语言ringbuffer如何实现循环队列高效操作?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文

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

c语言ringbuffer
(图片来源网络,侵删)

为什么使用环形缓冲区?

  1. 高效:相比于标准队列(需要频繁的 memmovemalloc/free),环形缓冲区的读写操作通常是 O(1) 的时间复杂度,非常高效。
  2. 内存友好:大小固定,避免了动态内存分配的开销和碎片化问题。
  3. 应用广泛:非常适合生产者-消费者模型,
    • 嵌入式系统:UART 串口通信、ADC/DAC 数据采集。
    • 操作系统:进程间通信、设备驱动。
    • 网络编程:数据包的缓存和处理。
    • 音频/视频流处理:缓冲音视频数据。

环形缓冲区的基本原理

想象一个固定大小的数组,它有两个“指针”:

  • 写入指针:指向下一个数据将要写入的位置。
  • 读取指针:指向下一个将要读取的数据的位置。

这两个指针在数组中循环移动。

核心状态判断

判断缓冲区是“空”还是“满”是环形缓冲区实现的关键,最简单的方法是使用一个额外的 size 变量来记录当前数据量,但更巧妙、更常用的方法是利用指针的位置关系,而无需额外的变量。

  • 缓冲区为空:当 读指针 == 写指针 时,缓冲区为空。
  • 缓冲区为满:当 (写指针 + 1) % 缓冲区大小 == 读指针 时,缓冲区为满,这里我们保留一个空位,是为了区分“空”和“满”的状态,避免了使用额外的标志位。

C 语言实现

下面是一个完整、可运行的 C 语言环形缓冲区实现,包含了结构体定义、初始化、写入、读取、状态查询等核心功能。

c语言ringbuffer
(图片来源网络,侵删)

定义结构体

#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 标志位来记录状态,也可以实现一个“写指针追上读指针”即为满的版本,但这会增加实现的复杂性。
  • 灵活性:这个实现非常灵活,可以轻松地集成到任何项目中。
c语言ringbuffer
(图片来源网络,侵删)
-- 展开阅读全文 --
头像
dede交叉栏目怎么样
« 上一篇 03-01
dede用户名密码忘了怎么找回?
下一篇 » 03-01

相关文章

取消
微信二维码
支付宝二维码

目录[+]