26、链表操作:链表节点删除时的内存泄漏问题
链表操作,说白了就是玩指针。我见过太多人,写链表删除写得飞起,结果一跑内存监控,泄漏得跟筛子似的。嗯,今天咱们就聊聊这个经典问题——删除节点时,内存到底去哪儿了?
一、先看一个“标准”的错误写法
很多人刚学链表时,都写过类似这样的代码:
// 单链表节点定义
typedef struct Node {
int data;
struct Node *next;
} Node;
// 删除指定值的节点(错误示范)
void delete_node_wrong(Node **head, int value) {
if (head == NULL || *head == NULL) return;
Node *cur = *head;
Node *prev = NULL;
while (cur != NULL) {
if (cur->data == value) {
// 找到了,删除当前节点
if (prev == NULL) {
*head = cur->next; // 删除头节点
} else {
prev->next = cur->next;
}
// 问题就在这里:没有 free(cur)!
// cur 这个节点从链表中断开了,但内存还在
break;
}
prev = cur;
cur = cur->next;
}
}
你看,代码逻辑上没问题——指针调整对了,链表结构没断。但内存呢?cur 指向的那块内存,从此成了孤儿。每次调用这个函数,就丢一块内存。跑个几千次,内存占用就上去了。
二、正确的删除操作:三步走
我个人习惯,写链表删除时脑子里过三个步骤:
- 找到节点 —— 遍历链表,定位目标
- 摘除节点 —— 调整前后指针,让目标脱离链表
- 释放内存 ——
free()掉目标节点
少一步,就是坑。来看正确写法:
void delete_node_correct(Node **head, int value) {
if (head == NULL || *head == NULL) return;
Node *cur = *head;
Node *prev = NULL;
while (cur != NULL) {
if (cur->data == value) {
// 摘除节点
if (prev == NULL) {
*head = cur->next; // 删除的是头节点
} else {
prev->next = cur->next;
}
// 释放内存 —— 这一步绝对不能省
free(cur);
cur = NULL; // 我习惯顺手置空,防止野指针
return;
}
prev = cur;
cur = cur->next;
}
}
你看,就多了两行代码。但这两行,决定了你的程序是“稳定运行”还是“跑着跑着就崩了”。
free() 之后立刻把指针置 NULL。虽然在这个函数里 cur 是局部变量,但养成习惯后,在复杂场景下能避免很多野指针问题。
三、双链表删除:别忘了两个方向的指针
单链表只涉及 next 指针,双链表就多了一个 prev。我在项目中遇到过有人只调整了 next,忘了改 prev,结果遍历时往回走就崩了。
// 双链表节点定义
typedef struct DNode {
int data;
struct DNode *prev;
struct DNode *next;
} DNode;
void delete_dnode(DNode **head, DNode *target) {
if (head == NULL || *head == NULL || target == NULL) return;
// 如果删除的是头节点
if (*head == target) {
*head = target->next;
}
// 调整前驱节点的 next 指针
if (target->prev != NULL) {
target->prev->next = target->next;
}
// 调整后继节点的 prev 指针 —— 很多人漏掉这里
if (target->next != NULL) {
target->next->prev = target->prev;
}
// 释放目标节点
free(target);
target = NULL;
}
你想想看,双链表每个节点有两个指针,删除时就得处理两个方向。漏一个,链表就“半残”了。
四、内存泄漏的“隐形杀手”:删除后继续使用
还有一种更隐蔽的泄漏——你以为删了,其实还在用。我曾经在项目里调试一个内存泄漏,查了两天才发现:
void delete_and_use_wrong(Node **head, int value) {
Node *cur = *head;
Node *prev = NULL;
while (cur != NULL) {
if (cur->data == value) {
if (prev == NULL) {
*head = cur->next;
} else {
prev->next = cur->next;
}
// 这里 free 了 cur
free(cur);
// 但后面又用 cur 去访问 next —— 这是未定义行为!
// 实际上 cur 的内存可能已经被回收了
break;
}
prev = cur;
cur = cur->next;
}
// 更糟糕的是,外面可能还持有 cur 的副本
}
free() 之后,cur 指向的内存已经归还给系统。你再读 cur->next,读到的可能是垃圾数据,也可能直接段错误。更可怕的是——如果这块内存被重新分配给其他变量,你还在改它,那就会莫名其妙地破坏其他数据。
五、避坑指南:我踩过的那些坑
我曾经在一个嵌入式项目里,用链表管理动态分配的传感器数据包。删除节点时忘了 free(),跑了三天,内存从 64KB 涨到 60MB,系统直接挂了。从那以后,我给自己定了几个规矩:
- 写删除函数时,先写 free,再写指针调整 —— 顺序反过来容易忘
- 每个 malloc 都要有对应的 free —— 写代码时就配对,别等调试时再补
- 删除后把指针置 NULL —— 虽然不能完全避免问题,但能提前暴露 bug
- 用 valgrind 或 AddressSanitizer 跑一遍 —— 工具能帮你找到肉眼漏掉的问题
六、知识体系图
下面这张图,帮你理清链表删除时内存管理的核心逻辑:
七、总结
链表删除的内存泄漏,说白了就一句话:摘了节点不 free,等于把垃圾留在屋里。你想想看,嵌入式系统内存本来就金贵,一个节点几十字节,泄漏几千次就是几十 MB。在资源受限的环境下,这直接导致系统崩溃。
我个人习惯,每次写完链表操作,都会用 valgrind 跑一遍。没跑过内存检查的链表代码,我是不敢往项目里放的。嗯,这习惯救过我很多次。
公众号:蓝海资料掘金营,微信deep3321