循环队列分析及应用
逐一解读队列模块
一、队列数据结构
queue.h#ifndef _QUEUE_H_
#define _QUEUE_H_#include <stdint.h>这是队列里面的关键参数,队列里面有一个数组,是用来接收数据的,那么就需要定义一个数组的下标,队头和队尾,接着就是队列的长度、最后是队列的数组。typedef struct
{uint32_t head; //数组下标,指向队头uint32_t tail; //数组下标,指向队尾uint32_t size; //队列缓存长度(初始化时赋值)uint8_t *buffer; //队列缓存数组(初始化时赋值)
} QueueType_t;用来表示队列状态,一共有四种状态,
1、正常状态
2、错误状态
3、队列已填满
4、队列已空
typedef enum
{QUEUE_OK = 0, //队列正常QUEUE_ERROR, //队列错误QUEUE_OVERLOAD, //队列已满QUEUE_EMPTY //队列已空
} QueueStatus_t;void QueueInit(QueueType_t *queue, uint8_t *buffer, uint32_t size);QueueStatus_t QueuePush(QueueType_t *queue, uint8_t data);QueueStatus_t QueuePop(QueueType_t *queue, uint8_t *pdata);uint32_t QueuePushArray(QueueType_t *queue, uint8_t *pArray, uint32_t len);uint32_t QueuePopArray(QueueType_t *queue, uint8_t *pArray, uint32_t len);uint32_t QueueCount(QueueType_t *queue);#endif
二、队列模块函数
1、初始化队列结构体
一般在调用队列之前需要进行初始化函数
#define MAX_BUF_SIZE (77)
static uint8_t g_rcvDataBuf[MAX_BUF_SIZE]实例化一个队列数据 主要包含队头、队尾、队列长度、队列的buff数组
static QueueType_t g_rcvQueue首先需要初始化这写个内容
因为QueueType_t里面的buff里面是指针,因此需要将我们自己定义的数组的首地址传进去注意数据类型QueueInit(&g_rcvQueue, g_rcvDataBuf, MAX_BUF_SIZE);void QueueInit(QueueType_t *queue, uint8_t *buffer, uint32_t size)
{queue->buffer = buffer;queue->size = size;queue->head = 0;queue->tail = 0;
}
QueueType_t * queue 这样就相当于将我们自己定义的结构体的首地址传进去,然后利用形参queue进行访问,而前面的QueueType_t 表示的是这个结构体的类型,也就是我们传进去地址类型,或者也可以理解为* queue指针变量的类型,再或者可以理解为我们使用编译器开辟了一个* queue把这个地址空间,而这个地址空间的结构和QueueType_t 是一模一样的,用来保存我们传进去的自己定义的结构体g_rcvQueue。理解可能有些绕,
可以参考我前面的文章。指针深入理解(一)-CSDN博客 指针深入理解(二)-CSDN博客等专栏文章关于这一部分的解读。
而我们的queue->buffer = buffer; 接收的也是我们自己定义数组的首地址,g_rcvDataBuf 这个表示数组名/数组标签 也就是数组的首地址。
2、压入数据到队列中
QueueStatus_t QueuePush(QueueType_t *queue, uint8_t data)
{uint32_t index = (queue->tail + 1) % queue->size;if (index == queue->head){return QUEUE_OVERLOAD;}queue->buffer[queue->tail] = data;queue->tail = index;return QUEUE_OK;
}
传进去的一个是环形队列的指针变量,一个是即将压入队列的data
一个data是一个字节,只能放进去一个字节,并且本身在硬件层面,一次也只能传输一个字节。一个字节发送完毕,产生一个中断。需要多少个字节,则会产生多少个中断。
首先在传进去之前,需要判断队列是否已经满了,如果满了,那么就退出函数。
需要说明的是tail这个变量指的是队尾,可是我们怎么知道这个队尾?
队尾是负责输入数据的。
根据我们编写的代码可以看出,tail只有在初始化的时候会进行赋值为0。后续没有将其赋值为0。
那么第一次压入的时候根据这个QueuePush函数的执行逻辑可以看出,tail会进行加1操作,并且会更新这个数据( queue->tail = index;)。因此正常情况下会一直进行这个操作。
循环队列会出现两种情况,一种是读的太快,写的太慢,head和tail会相等
另外一种情况就是tail走了一圈,和head重合了,这个是写的比读的快, 这一种相当于是队列满了,那么就不能再写入数据了,但是和第一种就会造成无法判断,因此为了避免这种情况,设计者就重新设计了一个思路,在数组中空出来一个位置,不写入数据。
也就是tail指向head前面的一个位置时,认为队列满了,这样就轻松区别了这两种情况。
而在代码实现上面就是下面这一行代码
uint32_t index = (queue->tail + 1) % queue->size;
并且这个取模运算就是实现了循环操作,由于tail和head之间隔了一个位置,那么就算这个时候tail已经走了一圈,由于head前面必须要保持一个空位置,因此,只有在以下这种情况才会是满
uint32_t index = (queue->tail + 1) % queue->size;if (index == queue->head){return QUEUE_OVERLOAD;}queue->buffer[queue->tail] = data;queue->tail = index;return QUEUE_OK;
如果我数组的长度是5,那么数值就是0-4.
假设我现在tail现在是在0号位置写入数据,但是在刚进来这个函数的时候已经tail+1了,也就是index是1,在写入0位置数据以后,就会将index赋值为tail,此时tail为1,但是是没有数据的。这样就实现了在head前面永远有一个位置是空的,或者可以理解为写入当前tail数据位置的下一次位置已经被空出来了,不可能被写入数据。因此下一次进来就会立马+1进行判断,判断是否紧挨着head的位置,如果是就不会写了,直接退出函数。如果不是就会写,是在不+1的位置写。这个地方需要认真理解,帮助后续的分析代码有很大帮助,或者说进行优化的时候,可以有思路,知其所以然。
这是写入一组数据到队列中,
道理是一样的,只不过使用了for循环遍历。
将这一组数据全部写到队列里面。
uint32_t QueuePushArray(QueueType_t *queue, uint8_t *pArray, uint32_t len)
{uint32_t i;for (i = 0; i < len; i++){if(QueuePush(queue, pArray[i]) == QUEUE_OVERLOAD){break;}}return i;
}
3、读队列的数据
首先将接收数组进行初始化操作。
然后是调用函数,
uint8_t readBuf[PACKET_DATA_LEN_MAX] = {0};QueuePop(&g_rcvQueue, &readBuf[0])
注意写入和读取两个函数的区别
QueueStatus_t QueuePop(QueueType_t *queue, uint8_t *pdata)
{if(queue->head == queue->tail){return QUEUE_EMPTY;}*pdata = queue->buffer[queue->head];queue->head = (queue->head + 1) % queue->size;return QUEUE_OK;
}
在读取之前先要判断队列是否为空,
就跟在写之前先判断是否为满一样,在工作用,一定要注意边界条件的处理,只有这样才能将产品维护的很好,业务干的漂亮,不然就是写出来Bug很多。
言归正传
当队列为空的时候,也就只有一种情况,我写的太慢了,读的速度很快,导致队列中一会数据就用完了。那就是head追上了tail,也只有这一种情况会这样。需要注意的是队列满的时候,是另外的判断思路。因此,满和空就完全是两个判断方式,直接让他们两种情况永远不会有交集。
此外,如果位置不是空,那就取出当前位置的数据,然后进行+1,因为我读取了这次的数据以后,我一定要直接+1处理,这样不会影响我下一次的判断。这就是编程的思想。
跟写入的思路是异曲同工之妙。
这是直接读出来一组数据,直接连续读数据。
一口气读出来。感觉这样容易导致空?
目前想想不到应用场景。
uint32_t QueuePopArray(QueueType_t *queue, uint8_t *pArray, uint32_t len)
{uint32_t i;for(i = 0; i < len; i++){if (QueuePop(queue, &pArray[i]) == QUEUE_EMPTY){break;}}return i;
}
四、获取队列中的数据个数
一般是两种情况,
uint32_t QueueCount(QueueType_t *queue)
{if (queue->head <= queue->tail){return queue->tail - queue->head;}return queue->size + queue->tail - queue->head;
}
以上就是循环队列的基本使用方法。
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
示例:
如果您在博客或文章中引用了本文的内容,请在显著位置标注类似以下声明:
本文部分内容参考自 [博主名称] 的原创文章,原文链接:[文章链接],遵循 CC 4.0 BY-SA 版权协议。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。*