C语言队列handle如何正确使用与实现?

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

这个概念非常重要,因为它体现了C语言中封装抽象的核心思想,让你可以像使用高级语言一样操作数据结构,而不必关心其内部复杂的实现细节。

c语言队列handle
(图片来源网络,侵删)

为什么需要“句柄”(Handle)?

在C++或Java等面向对象的语言中,你可以直接创建一个对象,Queue q = new Queue();,对象本身包含了数据和操作这些数据的方法。

但在C语言中,我们没有“类”和“对象”的概念,数据和方法是分离的,如果我们直接暴露队列的内部结构(比如用一个数组和一个头尾指针),那么任何代码都可以直接修改这些指针,导致队列状态混乱,非常不安全。

句柄(Handle) 就是为了解决这个问题而生,它本质上是一个不透明指针(Opaque Pointer)

  • 它是什么? 一个指向队列内部实现结构体的指针。
  • 它有什么用?
    1. 数据隐藏:用户只能拿到这个 void* 指针,但无法知道它指向的结构体里到底有什么成员(头指针、尾指针、数组等),用户无法直接修改队列的内部状态。
    2. 抽象接口:用户只需要通过一组固定的函数(如 Queue_Create, Queue_Enqueue, Queue_Dequeue)来操作这个句柄,而不关心队列是用数组实现的还是用链表实现的。

队列的实现方案

我们将采用两种常见的实现方式来展示如何创建句柄,你会发现句柄的设计让切换实现变得非常容易。

c语言队列handle
(图片来源网络,侵删)
  1. 基于数组的循环队列:更高效,有固定容量。
  2. 基于链表的队列:动态容量,更灵活。

方案一:基于数组的循环队列

这是最经典、最常见的实现方式。

1 定义内部结构

我们定义队列的“真身”,一个包含所有必要信息的结构体,这个结构体对用户是不可见的。

// queue_array.h (内部实现文件,用户通常不直接包含)
#ifndef QUEUE_ARRAY_H
#define QUEUE_ARRAY_H
// 定义队列元素类型,可以根据需要修改
typedef int QueueDataType;
// 队列的内部结构体
struct QueueStruct {
    QueueDataType* data;      // 存储队列元素的动态数组
    int capacity;             // 队列的总容量
    int front;                // 指向队头元素
    int rear;                 // 指向队尾元素的下一个位置
    int size;                 // 记录当前队列中的元素个数
};
// 将结构体指针重命名为 QueueHandle,作为对外暴露的句柄
typedef struct QueueStruct* QueueHandle;
#endif // QUEUE_ARRAY_H

关键点

  • front 指向第一个有效元素。
  • rear 指向最后一个有效元素的下一个位置,这种设计可以方便地判断队列是否为空(front == rear)。
  • size 变量让判断空/满的条件变得非常简单(size == 0 为空,size == capacity 为满),避免了浪费一个存储空间。

2 实现操作函数

这些函数是用户与队列交互的唯一途径。

c语言队列handle
(图片来源网络,侵删)
// queue_array.c
#include <stdio.h>
#include <stdlib.h>
#include "queue_array.h"
// 1. 创建队列
QueueHandle Queue_Create(int capacity) {
    if (capacity <= 0) {
        return NULL;
    }
    // 1. 为句柄结构体分配内存
    QueueHandle queue = (QueueHandle)malloc(sizeof(struct QueueStruct));
    if (!queue) {
        return NULL; // 内存分配失败
    }
    // 2. 为数据数组分配内存
    queue->data = (QueueDataType*)malloc(capacity * sizeof(QueueDataType));
    if (!queue->data) {
        free(queue); // 如果数据数组分配失败,释放之前分配的结构体
        return NULL;
    }
    // 3. 初始化队列成员
    queue->capacity = capacity;
    queue->front = 0;
    queue->rear = 0;
    queue->size = 0;
    return queue; // 返回句柄
}
// 2. 销毁队列
void Queue_Destroy(QueueHandle queue) {
    if (queue) {
        if (queue->data) {
            free(queue->data); // 先释放数据
        }
        free(queue); // 再释放句柄结构体
    }
}
// 3. 判断队列是否为空
int Queue_IsEmpty(QueueHandle queue) {
    if (!queue) {
        return 1; // 或者可以认为是错误,这里简单处理为空
    }
    return queue->size == 0;
}
// 4. 判断队列是否已满
int Queue_IsFull(QueueHandle queue) {
    if (!queue) {
        return 1; // 或者认为是错误
    }
    return queue->size == queue->capacity;
}
// 5. 入队
int Queue_Enqueue(QueueHandle queue, QueueDataType value) {
    if (!queue || Queue_IsFull(queue)) {
        return 0; // 失败
    }
    queue->data[queue->rear] = value;
    queue->rear = (queue->rear + 1) % queue->capacity; // 循环
    queue->size++;
    return 1; // 成功
}
// 6. 出队
int Queue_Dequeue(QueueHandle queue, QueueDataType* pValue) {
    if (!queue || Queue_IsEmpty(queue) || !pValue) {
        return 0; // 失败
    }
    *pValue = queue->data[queue->front];
    queue->front = (queue->front + 1) % queue->capacity; // 循环
    queue->size--;
    return 1; // 成功
}
// 7. 获取队头元素(不删除)
int Queue_Peek(QueueHandle queue, QueueDataType* pValue) {
    if (!queue || Queue_IsEmpty(queue) || !pValue) {
        return 0; // 失败
    }
    *pValue = queue->data[queue->front];
    return 1; // 成功
}

3 用户如何使用(main.c

用户只需要包含一个头文件,这个头文件只暴露函数和句柄类型,不暴露内部结构。

// main.c
// 假设我们有一个只暴露接口的头文件
#include <stdio.h>
// 用户代码应该包含这个头文件,它只包含函数声明和 QueueHandle 的 typedef
// 而不包含 struct QueueStruct 的定义
// 为了编译,我们这里假设它包含了 queue_array.h
// 在实际项目中,会有一个 public_queue.h 和 private_queue.h
#include "queue_array.h" 
int main() {
    // 1. 创建一个容量为5的队列
    QueueHandle myQueue = Queue_Create(5);
    if (!myQueue) {
        printf("Queue creation failed!\n");
        return -1;
    }
    // 2. 入队
    printf("Enqueuing 10, 20, 30...\n");
    Queue_Enqueue(myQueue, 10);
    Queue_Enqueue(myQueue, 20);
    Queue_Enqueue(myQueue, 30);
    // 3. 查看队头元素
    int frontValue;
    if (Queue_Peek(myQueue, &frontValue)) {
        printf("Front element is: %d\n", frontValue); // 应该输出 10
    }
    // 4. 出队
    printf("Dequeuing...\n");
    while (!Queue_IsEmpty(myQueue)) {
        int value;
        Queue_Dequeue(myQueue, &value);
        printf("Dequeued: %d\n", value);
    }
    // 5. 销毁队列
    Queue_Destroy(myQueue);
    myQueue = NULL; // 好习惯
    return 0;
}

方案二:基于链表的队列

我们来看如何用链表实现一个队列,你会发现对外提供的接口函数完全不变,这就是句柄的威力!

1 定义内部结构

// queue_list.h (内部实现文件)
#ifndef QUEUE_LIST_H
#define QUEUE_LIST_H
typedef int QueueDataType;
// 链表节点
struct QueueNode {
    QueueDataType data;
    struct QueueNode* next;
};
// 队列的内部结构体
struct QueueStruct {
    struct QueueNode* front; // 指向头节点
    struct QueueNode* rear;  // 指向尾节点
    int size;                // 可选,用于快速获取大小
};
// 同样,对外暴露的是句柄
typedef struct QueueStruct* QueueHandle;
#endif // QUEUE_LIST_H

2 实现操作函数

函数签名和数组版本完全一样!

// queue_list.c
#include <stdio.h>
#include <stdlib.h>
#include "queue_list.h"
QueueHandle Queue_Create(int capacity) {
    // 对于链表,capacity 参数可以忽略,或者用于预设一个初始大小
    (void)capacity; // 避免编译器警告
    QueueHandle queue = (QueueHandle)malloc(sizeof(struct QueueStruct));
    if (!queue) {
        return NULL;
    }
    queue->front = queue->rear = NULL;
    queue->size = 0;
    return queue;
}
void Queue_Destroy(QueueHandle queue) {
    if (!queue) return;
    struct QueueNode* current = queue->front;
    while (current) {
        struct QueueNode* next = current->next;
        free(current);
        current = next;
    }
    free(queue);
}
int Queue_IsEmpty(QueueHandle queue) {
    if (!queue) return 1;
    return queue->front == NULL;
}
int Queue_IsFull(QueueHandle queue) {
    // 链表队列通常不会满,除非内存耗尽
    (void)queue;
    return 0; 
}
int Queue_Enqueue(QueueHandle queue, QueueDataType value) {
    if (!queue) return 0;
    struct QueueNode* newNode = (struct QueueNode*)malloc(sizeof(struct QueueNode));
    if (!newNode) return 0; // 内存不足
    newNode->data = value;
    newNode->next = NULL;
    if (Queue_IsEmpty(queue)) {
        // 如果队列为空,新节点既是头也是尾
        queue->front = queue->rear = newNode;
    } else {
        // 否则,添加到队尾
        queue->rear->next = newNode;
        queue->rear = newNode;
    }
    queue->size++;
    return 1;
}
int Queue_Dequeue(QueueHandle queue, QueueDataType* pValue) {
    if (!queue || Queue_IsEmpty(queue) || !pValue) {
        return 0;
    }
    struct QueueNode* temp = queue->front;
    *pValue = temp->data;
    queue->front = queue->front->next;
    if (queue->front == NULL) {
        // 如果出队后队列为空,更新尾指针
        queue->rear = NULL;
    }
    free(temp);
    queue->size--;
    return 1;
}
int Queue_Peek(QueueHandle queue, QueueDataType* pValue) {
    if (!queue || Queue_IsEmpty(queue) || !pValue) {
        return 0;
    }
    *pValue = queue->front->data;
    return 1;
}

3 用户如何使用

用户的 main.c 代码完全不需要做任何修改!只需要链接不同的实现文件(queue_list.c 而不是 queue_array.c)即可。

// main.c (与数组版本完全相同)
#include "queue_list.h" // 注意!这里换成链表的头文件
int main() {
    QueueHandle myQueue = Queue_Create(10); // capacity 参数对链表无效,但接口一致
    if (!myQueue) {
        printf("Queue creation failed!\n");
        return -1;
    }
    printf("Enqueuing 10, 20, 30...\n");
    Queue_Enqueue(myQueue, 10);
    Queue_Enqueue(myQueue, 20);
    Queue_Enqueue(myQueue, 30);
    int frontValue;
    if (Queue_Peek(myQueue, &frontValue)) {
        printf("Front element is: %d\n", frontValue);
    }
    printf("Dequeuing...\n");
    while (!Queue_IsEmpty(myQueue)) {
        int value;
        Queue_Dequeue(myQueue, &value);
        printf("Dequeued: %d\n", value);
    }
    Queue_Destroy(myQueue);
    myQueue = NULL;
    return 0;
}

概念 解释 优点
句柄 一个指向内部数据结构的不透明指针 (void* 或特定结构体指针)。 数据隐藏:用户无法直接修改内部状态。接口抽象:操作统一,易于维护和切换实现。
内部结构 包含队列所有实现细节的结构体 (如 struct QueueStruct)。 封装了数据(数组/链表)和状态(头尾指针/大小)。
操作函数 接受 QueueHandle 作为参数,提供对队列的增、删、查、改等功能。 作为用户与队列交互的唯一合法途径,保证了封装性。

通过使用“句柄”模式,我们在C语言中实现了面向对象编程中的封装思想,使得代码更加健壮、安全和易于维护,这是一个在C语言中实现复杂数据结构时必须掌握的核心技巧。

-- 展开阅读全文 --
头像
C语言中equal函数如何实现字符串比较?
« 上一篇 今天
织梦后台左边导航怎么设置?
下一篇 » 今天

相关文章

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

目录[+]