环形数组(ringbuffer)

本文深入解析了环形数组的概念、优势及其实现细节,通过具体的代码示例展示了如何进行高效的数据存取操作,并探讨了其在多线程环境下的表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一:环形数组
1、概念

**百度百科:**是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流
**个人理解:**本身就是一个定长数组,在存储数据时,达到存储上限时会从0继续存储,也就是他在存储数据时是个闭环的过程,举个例子:如下图,假设是个 大小为9字节的数组,当存满了9个字节的数据时,在存入字节时就会从0开始存储,环形数组也称ringbuffer
在这里插入图片描述

2、优势

首先是一个先进先出的队列,当取走数据时不需要移动数组中的其他元素,在单消费者单生产者的模型下,不需要加锁,可以更快的存取数据。

3、实现

首先定义一个Ringbuffer的类,成员变量定义如下

 	unsigned char*m_pBuffer; //数组指针
    unsigned int m_nSize;    //数组大小
    unsigned int m_w_index;  //写索引    
    unsigned int m_r_index;  //读索引

成员函数申明如下(主要了解存取):

    unsigned int Put(const uint8_t *pBuffer, unsigned int nLen);
    unsigned int Get(uint8_t *pBuffer, unsigned int nLen);

以下图为参考来理解一个ringbuffer的实现,图中r_idx 为读索引,w_idx为写索引,大小为14, 当前为ringbuffer的初始状态。
在这里插入图片描述
举个例子:当存9个数据,取2个数据时,索引为下图状态
在这里插入图片描述
写数据分析
为了去除不必要的边界判断,加入2个前提
1、写入的数据长度大于ringbuffer可写入的长度,则忽略,也就是禁止写入。
2、如果数据长度 >=数组长度(达到容量上限)则禁止写入。
假设写入一个任意长度的数组,那么可能存在如下情况
情况1:写索引>=读索引,待写数据长 <= “右可写入部分”的长度,直接写入整个待写的数据
情况2:写索引>=读索引,待写数据长 > “右可写入部分”的长度,先写入右可写的长度,在把剩下的写入做可写部分在这里插入图片描述情况3:写索引<读索引,直接写入整个待写的数据在这里插入图片描述
写数据代码实现

/**
*	buffer: 待存入的数据指针
*	len: 待存入的数据长度
*/

unsigned int RingBuffer::Put(const unsigned char *buffer, unsigned int len)
{
    unsigned int cur_r = m_r_index;
    unsigned int datalen  = m_w_index - cur_r;
    //已经满了
    if(datalen >= m_nSize)
        return 0;
    //索引在数组的位置
    unsigned int t_w  = m_w_index   & (m_nSize - 1);
    unsigned int t_r = cur_r & (m_nSize - 1);
    if(t_w >= t_r){
    	//情况1
        if(len <= (m_nSize-t_w)){
            memcpy(m_pBuffer + t_w, buffer, len);
        }else{//情况1
            memcpy(m_pBuffer + t_w, buffer, (m_nSize-t_w));
            memcpy(m_pBuffer , buffer, len - (m_nSize-t_w));
        }
    }else{
    	//情况3
         memcpy(m_pBuffer + t_w , buffer, len);
    }
    m_w_index += len;
    return len;
}

读数据分析
同样的去除不必要的边界判断,加入2个前提:
1、数据长度 < 待读入的长度,则忽略
2、数据长度为0时(无数据可读),忽略
情况1:写索引>读索引,直接读取
在这里插入图片描述
情况2:写索引 <= 读索引,待读数据长 <= “右可读”的长度,直接读取待读长度
情况2:写索引 <= 读索引,待读数据长 > “右可读”的长度,先读取“右可读”的数据,在从“左可读”读取剩余部分
在这里插入图片描述
读数据实现

unsigned int RingBuffer::Get(unsigned char *buffer, unsigned int len)
{
    unsigned int cur_w = m_w_index;
    unsigned int datalen  = cur_w - m_r_index;
    //空了或小于
    if(datalen < len || datalen ==0 )
        return 0;
   
    //索引在数组的位置
    unsigned int t_w  = cur_w & (m_nSize - 1);
    unsigned int t_r = m_r_index & (m_nSize - 1);
    if(t_w > t_r){
        memcpy(buffer,m_pBuffer + t_r, len);
    }else{
            if(len <=  (m_nSize - t_r))
                memcpy(buffer,m_pBuffer + t_r, len);
            else{
                memcpy(buffer,m_pBuffer + t_r, (m_nSize - t_r));
                memcpy(buffer+(m_nSize - t_r),m_pBuffer, len - (m_nSize - t_r));
            }
    }
    m_r_index += len;
    return len;
}

不加锁分析
现在考虑2个线程,一个读一个写
当写数据时,我们会先去获取读索引的值,那么在读线程就有可能改变读索引(读索引只会变大,不会变小),此时的写索引在没获取到读索引值时是保持不变的,那么读索引不可能超过写索引的值的(读线程有判断),所以读索引的值改变范围始终会在下图箭头标注的区间,因此不管什么情况我们会在这个区间内拿到一个读索引,我们就得到一个可写入的数据区间,然后忘里面写数据,假设在写的过程读线程又来修改读索引,怎么办?此时已经无需关心读索引了,因为在此之前我们拿到了一个读索引副本,此后所有的读索引都比这个副本值大,此时无论我们怎么写入这个空间都不会影响到读线程去读数据
同理读数据不加锁分析也一样
在这里插入图片描述
多线程就不行了,假如2个线程同时写就有可能对一个地方重复写入

gitHub源码

在数据结构相关算法的实现中,错误排查和修正是一项重要的技能。尤其在学习或实践如《王道数据结构》这类教材中的算法时,常见的错误可能包括逻辑错误、边界条件处理不当、内存管理问题等。以下是一些常见的错误类型及修正方法: ### 1. 边界条件处理错误 在实现如链表操作、数组排序、树的遍历等算法时,边界条件的处理往往容易被忽略。例如,在链表的插入或删除操作中,如果没有正确处理头节点或尾节点的情况,可能导致程序崩溃或结果错误[^1]。 ```c // 错误示例:链表删除操作未处理头节点 void deleteNode(Node* head, int key) { Node* current = head; while (current->next != NULL && current->next->data != key) { current = current->next; } if (current->next != NULL) { Node* temp = current->next; current->next = current->next->next; free(temp); } } ``` 上述代码在删除节点时没有处理头节点的情况。修正方法是增加对头节点的判断: ```c // 修正示例:链表删除操作处理头节点 void deleteNode(Node** head, int key) { Node* temp = *head; if (*head != NULL && (*head)->data == key) { *head = temp->next; free(temp); return; } Node* current = *head; while (current != NULL && current->next != NULL && current->next->data != key) { current = current->next; } if (current != NULL && current->next != NULL) { Node* nodeToDelete = current->next; current->next = current->next->next; free(nodeToDelete); } } ``` ### 2. 内存泄漏 在动态分配内存时(如使用 `malloc` 或 `new`),如果未能及时释放不再使用的内存,会导致内存泄漏。例如,在实现栈或队列时,如果没有正确释放每个节点的内存,程序可能会占用越来越多的内存[^1]。 ```c // 错误示例:栈的销毁函数未释放所有节点 void destroyStack(Node* top) { while (top != NULL) { Node* temp = top; top = top->next; // 没有释放 temp 指向的内存 } } ``` 修正方法是在循环中释放每个节点的内存: ```c // 修正示例:栈的销毁函数释放所有节点 void destroyStack(Node** top) { while (*top != NULL) { Node* temp = *top; *top = (*top)->next; free(temp); } } ``` ### 3. 指针操作错误 指针操作是数据结构实现中最容易出错的部分之一。例如,在实现二叉树的遍历时,如果未能正确更新指针,可能导致无限循环或访问非法内存地址。 ```c // 错误示例:二叉树中序遍历指针操作错误 void inOrderTraversal(Node* root) { Stack* stack = createStack(); Node* current = root; while (current != NULL || !isEmpty(stack)) { while (current != NULL) { push(stack, current); current = current->left; } current = pop(stack); printf("%d ", current->data); current = current->right; // 错误:未正确更新 current } freeStack(stack); } ``` 上述代码中,`current` 的更新没有问题,但需要确保 `pop` 和 `push` 操作的正确性。修正方法是确保栈操作的正确性: ```c // 修正示例:二叉树中序遍历指针操作正确 void inOrderTraversal(Node* root) { Stack* stack = createStack(); Node* current = root; while (current != NULL || !isEmpty(stack)) { while (current != NULL) { push(stack, current); current = current->left; } current = pop(stack); printf("%d ", current->data); current = current->right; } freeStack(stack); } ``` ### 4. 逻辑错误 逻辑错误是指算法的逻辑流程存在缺陷,导致结果不正确。例如,在实现快速排序时,如果分区逻辑不正确,可能导致排序失败。 ```c // 错误示例:快速排序分区逻辑错误 int partition(int arr[], int low, int high) { int pivot = arr[high]; int i = low - 1; for (int j = low; j <= high; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return i + 1; } ``` 上述代码中,`for` 循环的条件 `j <= high` 导致 `j` 会遍历到 `high`,而 `pivot` 是 `arr[high]`,这会导致 `arr[j] < pivot` 的条件永远不成立。修正方法是将 `j` 的上限改为 `high - 1`: ```c // 修正示例:快速排序分区逻辑正确 int partition(int arr[], int low, int high) { int pivot = arr[high]; int i = low - 1; for (int j = low; j < high; j++) { if (arr[j] < pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); return i + 1; } ``` ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dai1396734

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值