现在是学到队列了

问题1

// 初始化函数(假设Queue结构体包含phead/ptail/size)
void QueueInit(Queue* pq)
{
    assert(pq);
    pq->phead = pq->ptail=NULL;
    pq->size = 0;
}

问:这里的assert检查的是什么?是pq指向的结构体是否为空吗

答:assert(pq); 检查的是 指向 Queue 结构体的指针 pq 本身不是空指针(即 pq 不是 NULL),和 Queue 结构体里有没有成员、成员的值是什么完全无关。

拆解 assert(pq); 的作用

1. 先理解 pq 是什么

在你的初始化函数 void QueueInit(Queue* pq) 中:

  • Queue* pq 是一个指针变量,它的作用是「指向内存中一个 Queue 结构体变量」;
  • 比如你在主函数里写 Queue q; QueueInit(&q);,此时 pq 接收的是 &q(变量 q 的地址),pq 指向的是实实在在的 Queue 结构体;
  • 但如果有人错误调用 QueueInit(NULL);,此时 pq 就是空指针(NULL),意味着它不指向任何内存地址。
2. assert(pq); 的本质:防错断言

assert 是 C 语言的断言宏,作用是:

  • 如果括号里的表达式为真(比如 pq != NULL),程序正常执行;
  • 如果表达式为假(pq == NULL),程序会立刻崩溃,并打印错误信息(告诉你哪一行的断言失败了)
3. 为什么必须加这个断言?

如果 pq 是 NULL,后续执行 pq->phead = NULL 就相当于「对空指针解引用」—— 这是 C 语言中最常见的运行时错误,会导致程序崩溃(段错误),而且崩溃时可能没有明确的错误提示。

加 assert(pq); 的目的是:在错误发生的源头直接终止程序,并明确提示 “pq 是空指针”,方便你快速定位问题,而不是让程序在后续执行 pq->phead 时莫名其妙崩溃

关键区分:「指针为空」vs「结构体成员为空」

(我搞错的地方)

概念含义例子
pq == NULL指针 pq 不指向任何内存地址(空指针)调用 QueueInit(NULL) 时,pq 就是 NULL
pq != NULL指针 pq 指向一块有效的 Queue 结构体内存调用 QueueInit(&q) 时,pq 指向变量 q
pq->phead == NULL指针 pq 指向的结构体中,phead 成员是空指针(队列暂无节点)初始化后 pq->ph

问题2

问:为什么不需要专门为 QNode(队列节点结构体)写初始化函数,而只需要初始化 Queue(队列结构体)

核心结论先明确

Queue 结构体需要初始化,是因为它是管理队列的 “容器”,必须先把它的核心状态(头尾指针、长度)置为初始值;而 QNode 是单个数据节点,它的初始化是在「创建节点的那一刻」(malloc 后)完成的,不需要单独写初始化函数。

Queue q; // 定义一个队列结构体变量

此时 q 分配在栈上,但它的成员(pheadptailsize)的值是随机的垃圾值(栈内存不会自动清零):

  • q.phead 可能是一个随机的野指针;
  • q.size 可能是一个随机的数字(比如 4195835)。

如果不调用 QueueInit(&q) 初始化,后续执行 QueuePush(&q, 10) 时:

  • 代码判断 q.phead == NULL 会出错(因为 q.phead 是野指针,不是 NULL);
  • 可能导致内存访问错误、程序崩溃。

所以 Queue 必须初始化,目的是把它的成员置为 “空队列” 的合法初始状态(phead=NULLptail=NULLsize=0

 QNode 结构体:无需单独初始化,而是 “按需创建 + 即时初始化”

QNode 是存储单个数据的节点,它的创建时机是「入队时」,且是动态分配内存malloc),而非提前定义在栈上:

// 入队时才创建节点,创建时直接初始化
QNode* newNode = (QNode*)malloc(sizeof(QNode));
assert(newNode); // 确保内存分配成功
newNode->val = val;   // 初始化节点的数值
newNode->next = NULL; // 初始化节点的next指针(队尾节点的next必须是NULL)
补充:如果强行给 QNode 写初始化函数会怎样?

也可以写,但完全没必要,反而增加代码冗余

核心区别Queue 是提前存在的 “管理结构”,需要初始化为合法空状态;QNode 是按需创建的 “数据载体”,创建时直接初始化即可。

问题3

问: 为什么main 函数里只需要创建 Queue 结构体变量,而不用直接创建 QNode

拆解:Queue 和 QNode 的角色定位(核心原因)

我们先明确两者在队列结构中的不同角色:

结构体角色定位使用者(main)是否需要直接操作原因
Queue队列的 “管理层 / 控制中心”✅ 必须创建,作为操作入口封装了队列的核心状态(头尾指针、长度),是所有队列操作的唯一入口
QNode队列的 “数据载体 / 底层节点”❌ 完全不需要,由底层函数管理只是存储单个数据的链表节点,属于实现细节,使用者无需感知
Queue:你操作的是 “队列” 这个抽象概念,而非节点

在 main 函数中,你要的是 “一个可以存数据、取数据的队列”,而不是 “一堆零散的节点”。比如:

int main() {
    Queue q;          // 创建“队列”这个整体
    QueueInit(&q);    // 初始化队列
    QueuePush(&q, 10);// 往队列里加数据(不用关心底层怎么创建节点)
    QueuePrint(&q);   // 打印队列(不用关心怎么遍历节点)
    return 0;
}

总结

  1. 抽象 vs 实现Queue 是队列的抽象接口,面向使用者(main);QNode 是底层实现细节,面向开发者(写队列函数的人),使用者无需感知;
  2. 封装的意义:创建 Queue 而非 QNode,能让你聚焦 “队列的功能”(入队、出队),而非 “节点的管理”(创建、串联、释放),降低使用复杂度;
  3. 操作入口Queue 是所有队列操作的唯一入口(封装了头尾指针和长度),直接操作 QNode 会失去队列的管理逻辑,导致数据结构混乱。

简单来说,你使用的是 “队列” 这个工具,而不是 “构成队列的节点零件”,这就是为什么 main 里只需要创建 Queue

问题4

225. 用队列实现栈 - 力扣(LeetCode)

释放空间时,如果是这样的

他只会释放obj指向的q1,q2。而phead,ptial指向的并没有被释放掉

先把后边指向的释放,在释放最外层的

问题5

1. QueuePop 函数:“只有一个节点” 的判断逻辑完全错误

错误代码:

if (pq->size == 0)//只有一个节点-->尾指针也要处理
{
    free(pq->phead);
    pq->phead = pq->ptail=NULL;
}

问题本质

  • 前面已经用 assert(pq->phead) 保证队列非空size ≥ 1),但你这里判断 size == 0(队列为空),这个分支永远不会执行;
  • 你原本想判断 “只有一个节点”,但条件写错了,导致:
    • 当队列只剩 1 个节点时,会走 else 分支,释放 phead 后 ptail 还指向已释放的内存(野指针);
    • 后续访问 ptail 会触发内存越界 / 崩溃。
2. QueueDestroy 函数:遍历节点的逻辑错误

错误代码:

QNode* next = pq->phead->next;//销毁要存储下一个节点

问题本质

  • 遍历销毁节点时,pq->phead 并不会随着 cur 移动而更新,始终指向原队列头;
  • 当 cur 移动到第二个节点后,pq->phead->next 还是原来的第二个节点,导致:
    • 无限循环(cur 永远指向第二个节点,无法遍历完所有节点);
    • 内存泄漏(除了第一个节点,其余节点都无法释放)。

问题6

622. 设计循环队列 - 力扣(LeetCode)

注意此题的tial指向的是队尾的下一个位置,而非队尾。为什么呢:因为初始化时tial=0

myCircularQueueCreate 中未给 obj->a 赋值

错误代码:

int* a=(int*)malloc(sizeof(int)*(k+1));
pst->head=0;
pst->tail=0;
pst->k=k;
return pst;

问题:你只开辟了数组内存,但没把数组地址赋值给 pst->a,导致后续访问 obj->a[obj->tail] 时访问野指针,直接崩溃

应该是

pst->a=(int*)malloc(sizeof(int)*(k+1));

问题7

问:销毁怎么销毁啊,销毁啥啊

// 释放内存
void myCircularQueueFree(MyCircularQueue* obj) {
   free(obj->a);
   free(obj);

为什么要这样

先理解:MyCircularQueue 的内存分布

我们先回顾创建队列时的内存申请过程,这是理解释放的关键:

MyCircularQueue* myCircularQueueCreate(int k) {
    // 第一步:申请「队列结构体」的内存(存在堆上)
    MyCircularQueue* pst = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    // 第二步:申请「存储数据的数组」的内存(也在堆上)
    pst->a = (int*)malloc(sizeof(int) * (k + 1));
    // ... 初始化head/tail/k
    return pst;
}

创建时我们做了两次 malloc,对应两块独立的堆内存:

  1. pst 指向的是「队列管理结构体」(存 head/tail/k/a 这些信息);
  2. pst->a 指向的是「实际存数据的数组」(比如你存的 1、2、3 这些值)。

堆内存的规则是:谁申请(malloc),谁释放(free,否则会导致内存泄漏

拆解 myCircularQueueFree 的两步操作

void myCircularQueueFree(MyCircularQueue* obj) {
   free(obj->a);  // 第一步:释放「数据数组」的内存
   free(obj);     // 第二步:释放「队列结构体」的内存
}
第一步:free(obj->a) —— 释放数据数组
  • obj->a 是指向 “存储队列元素的数组” 的指针(比如你入队的 1、2、3 都存在这里);
  • 如果不先释放 obj->a,直接释放 obj 后,obj->a 的地址就丢了,再也找不到这个数组,这块内存就永远占用着(内存泄漏);
  • 必须先释放 obj->a,因为 obj 里存着 a 的地址,释放 obj 前要先处理它指向的资源。
第二步:free(obj) —— 释放队列管理结构体
  • obj 是指向 “队列结构体” 的指针(存 head/tail/k 这些管理信息);
  • 这是创建时第一步 malloc 申请的内存,最后要释放;
  • ❗️注意顺序:必须先释放 obj->a,再释放 obj。如果反过来,释放 obj 后 obj 变成野指针,再访问 obj->a 会直接崩溃。

用生活例子类比

把这个过程比作 “租房子 + 买家具”:

  1. 创建队列(myCircularQueueCreate):
    • 租了一间 “管理室”(obj 指向的结构体);
    • 买了一批 “家具”(obj->a 指向的数组),放在管理室里;
  2. 释放队列(myCircularQueueFree):
    • 先把 “家具” 搬走(free(obj->a)),否则退租后家具就拿不出来了;
    • 再退掉 “管理室”(free(obj))。

常见误区提醒

  1. 只释放 obj,不释放 obj->a:会导致数组内存泄漏,程序运行久了会占满内存;
  2. 先释放 obj,再释放 obj->a:释放 obj 后 obj 是野指针,访问 obj->a 会触发程序崩溃;
  3.  正确顺序:先释放 “子资源”(obj->a),再释放 “父资源”(obj)。

总结

  1. free(obj->a) 是释放存储队列元素的数组内存,避免数据区内存泄漏;
  2. free(obj) 是释放管理队列的结构体内存,避免管理区内存泄漏;
  3. 释放顺序必须是「先子后父」(先 obj->a,后 obj),否则会崩溃或泄漏。

我的错误写法

void myCircularQueueFree(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    {
        return;
    }
    int tmp=obj->k;
    while(k--)
    {
        obj->head--;
        obj->head%=obj->tmp+1;
    }
    free(obj->a);
    free(obj);
}
1. 语法错误:变量名写错 + 未定义
int tmp=obj->k;
while(k--)  // 错误1:k是结构体的成员,应该用obj->k,直接写k未定义
{
    obj->head--;
    obj->head%=obj->tmp+1; // 错误2:tmp是局部变量,应该写tmp+1,不是obj->tmp(obj没有tmp成员)
}

这两行代码编译都通不过,更别说运行了。

2. 逻辑错误:head-- 会导致下标越界

循环队列的 head 是无符号逻辑下标,你反复执行 obj->head--:(应该是想++的)

  • 如果 head 初始是 0,head-- 会变成负数,访问 obj->a[负数] 直接触发数组越界崩溃;
  • 就算 head 不是 0,这种操作也没有任何意义 —— 释放内存和 head/tail 的位置无关。

二、核心问题:你误解了「free 的作用」

free 的核心规则:free(p) 只做一件事:把 p 指向的整块堆内存归还给系统,和内存里存了什么、指针位置无关

 你试图通过移动 head 来 “清空数据”,这是完全多余的 ——free 会直接回收整块数组内存,里面的所有数据都会被系统标记为 “可复用”,不需要手动 “清空”。

举个例子:你用 malloc 申请了一块能存 4 个 int 的数组(k=3 时,数组大小是 4),这块内存的地址是 0x1000 ~ 0x100F

  • 不管你往里面存了 1、2、3,还是移动 head/tail 到任何位置;
  • 只要执行 free(obj->a),系统会直接回收 0x1000 ~ 0x100F 这整块内存,里面的所有数据都会失效,不需要你手动改 head 或删数据。

为什么你的逻辑完全没必要?

1. 「判空才释放」是错误的
if(myCircularQueueIsEmpty(obj)) { return; }

就算队列是空的(head==tail),obj->a 指向的数组内存依然是你用 malloc 申请的,必须释放 —— 否则就算队列里没数据,这块内存也会泄漏。

2. 「循环移动 head」是无效的

移动 head 只是改变了结构体里一个整数的值,既不会清空数组里的数据,也不会帮助释放内存。比如数组里存了 1、2、3,你把 head 从 0 移到 3,数组里的 1、2、3 依然存在,直到 free(obj->a) 回收整块内存。

问题8

如下

int myCircularQueueRear(MyCircularQueue* obj) {
    //判断是否空;
    if(myCircularQueueIsEmpty(obj))//不是传&obj,这里的obj就是一级指针❗️
    {
        return -1;
    }
    else
    {
        return obj->tail==0?obj->a[obj->k]:obj->a[obj->tail-1];
    }
}


 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐