C语言队列初始化常见问题解析
现在是学到队列了
问题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 分配在栈上,但它的成员(phead、ptail、size)的值是随机的垃圾值(栈内存不会自动清零):
q.phead可能是一个随机的野指针;q.size可能是一个随机的数字(比如 4195835)。
如果不调用 QueueInit(&q) 初始化,后续执行 QueuePush(&q, 10) 时:
- 代码判断
q.phead == NULL会出错(因为q.phead是野指针,不是 NULL); - 可能导致内存访问错误、程序崩溃。
所以 Queue 必须初始化,目的是把它的成员置为 “空队列” 的合法初始状态(phead=NULL、ptail=NULL、size=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;
}
总结
- 抽象 vs 实现:
Queue是队列的抽象接口,面向使用者(main);QNode是底层实现细节,面向开发者(写队列函数的人),使用者无需感知; - 封装的意义:创建
Queue而非QNode,能让你聚焦 “队列的功能”(入队、出队),而非 “节点的管理”(创建、串联、释放),降低使用复杂度; - 操作入口:
Queue是所有队列操作的唯一入口(封装了头尾指针和长度),直接操作QNode会失去队列的管理逻辑,导致数据结构混乱。
简单来说,你使用的是 “队列” 这个工具,而不是 “构成队列的节点零件”,这就是为什么 main 里只需要创建 Queue。
问题4
释放空间时,如果是这样的

他只会释放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会触发内存越界 / 崩溃。
- 当队列只剩 1 个节点时,会走
2. QueueDestroy 函数:遍历节点的逻辑错误
错误代码:
QNode* next = pq->phead->next;//销毁要存储下一个节点
问题本质:
- 遍历销毁节点时,
pq->phead并不会随着cur移动而更新,始终指向原队列头; - 当
cur移动到第二个节点后,pq->phead->next还是原来的第二个节点,导致:- 无限循环(
cur永远指向第二个节点,无法遍历完所有节点); - 内存泄漏(除了第一个节点,其余节点都无法释放)。
- 无限循环(
问题6
注意此题的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,对应两块独立的堆内存:
pst指向的是「队列管理结构体」(存head/tail/k/a这些信息);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会直接崩溃。
用生活例子类比
把这个过程比作 “租房子 + 买家具”:
- 创建队列(
myCircularQueueCreate):- 租了一间 “管理室”(
obj指向的结构体); - 买了一批 “家具”(
obj->a指向的数组),放在管理室里;
- 租了一间 “管理室”(
- 释放队列(
myCircularQueueFree):- 先把 “家具” 搬走(
free(obj->a)),否则退租后家具就拿不出来了; - 再退掉 “管理室”(
free(obj))。
- 先把 “家具” 搬走(
常见误区提醒
- 只释放
obj,不释放obj->a:会导致数组内存泄漏,程序运行久了会占满内存; - 先释放
obj,再释放obj->a:释放obj后obj是野指针,访问obj->a会触发程序崩溃; - 正确顺序:先释放 “子资源”(
obj->a),再释放 “父资源”(obj)。
总结
free(obj->a)是释放存储队列元素的数组内存,避免数据区内存泄漏;free(obj)是释放管理队列的结构体内存,避免管理区内存泄漏;- 释放顺序必须是「先子后父」(先
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];
}
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)