【数据结构】关于环形队列库的改进办法

在之前发布的博客《【数据结构】环形队列(循环队列)学习笔记总结》中,分享了我本人编写用 C 语言编写的环形队列库。该库实现了对环形队列的基本管理和操作,也应用在实践的项目中,并解决了项目中出现的问题。

迫于项目捉襟见肘的周期,在编写环形队列库时,并没有过多考虑扩展性和兼容性,因此,现有的环形队列库从只支持 uint8_t 类型的数组。如果遇到项目要求为其它类型,则该库将不再适用。

本着精益求精的原则,本文将对该库做一下优化改进。改进的目的为,使环形队列库从只支持 uint8_t 类型扩展为支持任意常见类型的数组,方法就是通过使用泛型编程的思想来进行优化。当然,C 语言本身并不支持直接的泛型,但可以将数据存储部分定义为void*指针,并且使用 sizeof() 来实现对不同类型数据的支持。

优化步骤

  1. 修改环形队列的存储结构: 通过使用 void* 来存储任意类型的数据,队列中需要存储的元素大小通过 size_t element_size 来确定。
  2. 修改操作函数: 环形队列的插入和删除等操作需要根据 element_size 来进行内存操作,数据存储和读取时使用 memcpy
  3. 动态分配内存: 环形队列初始化时,如果没有指定已存在的队列,则可以根据元素大小动态分配适当大小的内存空间。

优化后的环形队列库

环形队列结构体定义

对于环形队列结构体主要有三个方面改动:

  1. 使用 void* 来存储任意类型的数据;
  2. 新增结构体成员 size_t element_size ,表示队列的每个元素的大小(所占字节数);
  3. data 成员变量外,其余变量的类型全部改为 size_t
typedef struct {
    
    
    void *data;             // 存储数据的数组
    size_t head;            // 队头索引
    size_t tail;            // 队尾索引
    size_t capacity;        // 队列容量(元素数量)
    size_t element_size;    // 每个元素的大小
    size_t count;           // 当前队列中的元素数量
} circular_queue_t; 

[!NOTE]

关于 size_t 这个数据类型,我在《【面试题分享】重现 string.h 库常用的函数》的附录部分有提到,感兴趣的可以去看看。

初始化函数

原来的初始化函数,对传入的参数没有做任何判断,如果调用函数时,传入的参数有误,就会影响后续的操作。因此,对于初始化函数的优化有以下方面:

  1. 增加返回值,返回值的数据类型为 circular_queue_t *,即环形队列结构体的结构体指针;

  2. 对每个传入函数的参数都进行判断,具体如下:

    • 如果 capacityelement_size 任意一个参数为零值,这属于非法参数,直接返回 NULL
    • 判断 queue 是否为空指针,如果为空指针,则为该结构体分配所需空间;
    • 判断 data 是否为空指针,如果为空指针,则根据 capacityelement_size,为环形队列分配所需空间。
  3. 函数新增两次对内存空间的申请,任意一次申请失败都返回 NULL。第二次申请内存失败,需要先将第一次申请的空间释放,再返回 NULL

circular_queue_t* circular_queue_init(circular_queue_t *queue, void *data, size_t capacity, size_t element_size)
{
    
    
    if (!capacity || !element_size)
        return NULL;

    if (NULL == queue) {
    
    
        queue = (circular_queue_t *)malloc(sizeof(circular_queue_t));
        if (NULL == queue)
            return NULL;
    }

    if (NULL == data) {
    
    
        queue->data = malloc(capacity * element_size);
        if (NULL == queue->data) {
    
    
            free(queue);
            return NULL;
        }
    } else {
    
    
        queue->data = data;
    }

    queue->head = 0;
    queue->tail = 0;
    queue->capacity = capacity;
    queue->element_size = element_size;
    queue->count = 0;

    return queue;
}

[!NOTE]

不同的项目需求,对于环形队列和环形队列的管理结构体有不同的可能性:

  • 定义了环形队列,未定义环形队列的管理结构体;
  • 定义了环形队列的管理结构体,未定义环形队列;
  • 二者都定义了;
  • 二者均未定义。

为了让初始化函数适配这些可能性,因此做了如上的优化改进,这样提高了代码的灵活性、安全性和健壮性。

入队操作和出队操作

出队和入队在操作上有一些改进,原型是直接对已知类型的数组元素进行操作,现在队列的元素类型不确定,所以需要通过指针偏移和内存复制的方式来进行出入队的操作。具体改进如下:

入队函数:

bool circular_queue_enqueue(circular_queue_t *queue, const void *value)
{
    
    
    if (circular_queue_is_full(queue) || NULL == value || NULL == queue)
        return false;

    char *base = (char *)queue->data;
    char *tail_pos = base + (queue->tail * queue->element_size);
    memcpy(tail_pos, value, queue->element_size);

    queue->tail = (queue->tail + 1) % queue->capacity;
    queue->count++;
    return true;
}

出队函数:

bool circular_queue_dequeue(circular_queue_t *queue, void *value)
{
    
    
    if (circular_queue_is_empty(queue) || NULL == value || NULL == queue)
        return false;

    char *base = (char *)queue->data;
    char *head_pos = base + (queue->head * queue->element_size);
    memcpy(value, head_pos, queue->element_size);

    queue->head = (queue->head + 1) % queue->capacity;
    queue->count--;
    return true;
}

[!NOTE]

这里用的方法都比较统一,先定义一个 char* 类型的指针 base,并将队列指针强转为 char* 型后赋值给 base,这一步的目的是为了,后面指针偏移都能以一个字节做为基准(void* 指针没办法偏移)。

后续不管是找到队头还是队尾的位置,都是基于 base 指针偏移的,都是队头或者队尾的位置值乘以队列元素的类型长度,这样可以保证准确地找到队头或者队尾。

查看队头或队尾

查看队头或队尾元素的操作与入队和出队的操作类似,这里不做赘述。

查看队头元素:

bool circular_queue_peek_head(const circular_queue_t *queue, void *value)
{
    
    
    if (circular_queue_is_empty(queue) || NULL == value || NULL == queue)
        return false;

    char *base = (char *)queue->data;
    char *head_pos = base + (queue->head * queue->element_size);
    memcpy(value, head_pos, queue->element_size);
    return true;
}

查看队尾元素:

bool circular_queue_peek_tail(const circular_queue_t *queue, void *value)
{
    
    
    if (circular_queue_is_empty(queue) || NULL == value || NULL == queue)
        return false;

    char *base = (char *)queue->data;
    char *tail_pos = base + ((queue->tail - 1 + queue->capacity) % queue->capacity) * queue->element_size;
    memcpy(value, tail_pos, queue->element_size);
    return true;
}

清空队列函数

清空队列的操作与之前变化不大,主要是修改了内存清除的大小。

void circular_queue_clear(circular_queue_t *queue)
{
    
    
    queue->head = 0;
    queue->tail = 0;
    queue->count = 0;
	memcpy(queue->data, 0, queue->capacity * queue->element_size);
	queue->capacity = 0;
} 

新增队列销毁函数

由于初始化函数中存在 malloc 函数,考虑到对不再操作内存要进行释放和回收,所以新增了销毁队列的函数,具体如下:

void circular_queue_destroy(circular_queue_t *queue)
{
    
    
    if (queue == NULL)
        return;

    if (queue->data != NULL) {
    
    
        free(queue->data);
        queue->data = NULL;
    }

    free(queue);
}

不过这个函数在调用过程中,有一定的风险性,在 C 语言中,无法直接判断传入的指针是否合法(即判断它是否指向有效的内存)。前面在改进初始化函数时,就提到了环形队列和管理环形队列的结构体有四种情况,只要任意一个不是靠动态分配创建的,那么调用队列销毁函数就会导致程序崩溃。

当然,为了能愉快地使用这个库,这里再总结了一些经验,可以避免因传入非法指针导致的崩溃和未定义行为:

  1. 检查指针是否为 NULL

    circular_queue_destroy 函数中,可以首先检查传入的指针是否为 NULLfree 函数可以安全地处理 NULL 指针,因此提前进行这个判断可以避免释放空指针导致的错误。

    这点已经在前面的程序中体现了。

  2. 防止重复释放

    如果多次调用 circular_queue_destroy,可能会导致重复释放指针并引发未定义行为。为了解决这个问题,可以在释放指针后将指针设置为 NULL,这样就能避免在下一次调用时再次释放同一块内存。

    void circular_queue_destroy(circular_queue_t **queue)
    {
          
          
        if (queue == NULL || *queue == NULL)
            return;
    
        if ((*queue)->data != NULL) {
          
          
            free((*queue)->data);
            (*queue)->data = NULL;
        }
    
        free(*queue);
        *queue = NULL;
    }
    

    这里将 circular_queue_destroy 的参数改为 circular_queue_t **queue,传入的是指向 queue 指针的指针,这样在函数内部可以将原始指针置为 NULL,避免外部代码再去使用已经释放的指针。

  3. 设计层次上的约束

    在设计上确保只会传递有效的指针给 circular_queue_destroy。可以通过以下手段避免传递无效的指针:

    • 初始化后分配指针: 所有的动态分配内存只能通过特定的初始化函数完成,确保每次分配的指针是有效的。
    • 使用封装函数管理内存: 包装所有内存分配和释放操作,确保指针的生命周期在受控环境中管理。
    • 逻辑检查:在使用指针前,通过程序逻辑确保没有非法指针传递给 circular_queue_destroy
  4. 自定义内存管理函数

如果担心指针的有效性,可以通过封装 mallocfree 等函数,并在全局数据结构中维护分配的指针表,来确保所有传递给 free 的指针都是合法分配过的内存地址。不过这种方式实现起来相对复杂,适用于非常复杂的系统。

示例

#include <stdio.h>
#include "circular_queue.h"

int main()
{
    
    
    circular_queue_t *queue = circular_queue_init(NULL, NULL, 10, sizeof(int));

    // 入队
    for (int i = 0; i < 5; ++i)
        circular_queue_enqueue(queue, &i);

    // 出队
    int value;
    while (!circular_queue_is_empty(queue))
        if (circular_queue_dequeue(queue, &value))
            printf("%d ", value);

    printf("\n");

    // 销毁队列
    circular_queue_destroy(&queue);

    return 0;
}

仓库链接

其他改动不再一一赘述,可以直接到仓库查看。

GitHub:zhengxinyu13/my_circular_queue (github.com)

Gitee:Grayson_Zheng/my_circular_queue (gitee.com)

GitCode:my_circular_queue - GitCode

猜你喜欢

转载自blog.csdn.net/qq_42417071/article/details/142901914