26、链表的常见错误:野指针、内存泄漏、死循环、断链

链表这东西,写起来爽,调试起来……嗯,你懂的。我做了这么多年嵌入式,见过太多人在链表上栽跟头。说白了,链表本身不复杂,复杂的是指针操作带来的各种“幽灵”问题。

今天咱们就把这四个最常见的坑——野指针、内存泄漏、死循环、断链——一个一个掰开揉碎了讲清楚。每个我都踩过,有些还不止一次。

核心观点:链表错误的本质,是“内存状态”和“指针指向”不同步。你脑子里想的链表结构,和内存里实际的样子,不是一回事。

26.1 野指针:你指向的地址,早就不是你的了

野指针,就是指向“已释放”或“未分配”内存的指针。你用它去读写数据,轻则读到垃圾值,重则直接段错误。

我遇到过最典型的一个场景:一个同事在删除节点后,没有把原指针置NULL,后面又用这个指针去访问节点数据。结果程序跑了两小时才崩溃,查得我们头皮发麻。

野指针的常见成因

  • free()之后没置NULL——这是最常见的。free只是把内存归还给系统,但指针的值还在。
  • 返回局部变量的地址——函数返回后,栈空间被回收,指针变成野的。
  • 指针越界访问——比如你只分配了3个节点,却访问了第5个。
// 错误示例:free后未置NULL
Node *p = (Node*)malloc(sizeof(Node));
// ... 使用p ...
free(p);
// 此时p是野指针!
if (p != NULL) {   // 这个判断毫无意义
    p->data = 10;  // 危险!
}

// 正确做法:
free(p);
p = NULL;  // 手动置NULL

警告:free之后一定要置NULL。这不是形式主义,是保命操作。我习惯在free之后立即写 p = NULL,养成肌肉记忆。

26.2 内存泄漏:你申请的内存,再也找不回来了

内存泄漏,说白了就是“借了不还”。你每次malloc一块内存,用完不free,系统就永远少一块。嵌入式设备内存本来就金贵,泄漏几次就挂了。

我曾经维护过一个通信网关程序,跑三天就重启一次。查了整整一周,发现是链表插入时申请了新节点,但删除节点时只改了指针,忘了free。每次删除泄漏几十个字节,三天下来累积到几MB,系统就崩了。

内存泄漏的典型场景

  • 删除节点时只改链,不free——节点从链表摘下来了,但内存还在。
  • 覆盖指针前没释放旧内存——比如 p = malloc(...); 然后 p = malloc(...); 第一次分配的内存就丢了。
  • 异常分支忘记free——函数中间return了,前面的malloc没释放。
// 错误示例:删除节点时泄漏
void deleteNode(Node **head, int key) {
    Node *cur = *head, *prev = NULL;
    while (cur != NULL && cur->data != key) {
        prev = cur;
        cur = cur->next;
    }
    if (cur == NULL) return;
    
    if (prev == NULL) {
        *head = cur->next;  // 摘下来了
        // 但没free(cur)!泄漏了!
    } else {
        prev->next = cur->next;
        // 同样没free(cur)
    }
}

// 正确做法:摘下来之后一定要free
if (prev == NULL) {
    *head = cur->next;
} else {
    prev->next = cur->next;
}
free(cur);  // 释放内存
cur = NULL;

小技巧:我习惯在写malloc的同时,就把对应的free写好。比如先写 free(p); p=NULL; 再回头写中间的逻辑。这叫“配对编程法”,能有效减少泄漏。

26.3 死循环:你的链表,变成了一个圈

死循环在链表里,通常是因为。本该是单向的链表,某个节点的next指回了前面的节点,遍历就永远出不来了。

你想想看,while(p != NULL) { p = p->next; } 如果链表里有环,这个循环永远不会结束。CPU占用率直接飙到100%,系统卡死。

死循环的常见原因

  • 尾节点的next没置NULL——新节点申请后,next指向了随机值。
  • 插入操作搞错了指针顺序——比如先改了当前节点的next,再改前驱节点的next,结果形成了环。
  • 循环链表忘记判断头节点——遍历循环链表时,没有用“回到头节点”作为终止条件。
// 错误示例:尾节点next未置NULL
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->data = 100;
// 忘记设置 newNode->next = NULL;
// 此时newNode->next是随机值,遍历时会跑到未知地址

// 正确做法:
newNode->next = NULL;  // 必须显式初始化

如何检测环?快慢指针法:快指针每次走两步,慢指针每次走一步。如果相遇,说明有环。我在面试时经常问这个,能答上来的,链表基础基本过关。

26.4 断链:链表断了,后面的节点全丢了

断链,就是你在插入或删除节点时,指针操作顺序不对,导致链表中间断开。后面的节点虽然还在内存里,但你已经找不到它们了。

我记得有一次调试一个多线程任务队列,插入节点后,队列里的任务莫名其妙少了一半。查到最后,发现是插入时先改了当前节点的next,导致后面的节点全部丢失。嗯,这个坑我印象特别深。

断链的典型场景

  • 插入时先改当前节点,再改前驱节点——顺序反了,后面的节点就丢了。
  • 删除时没保存下一个节点的地址——直接free了当前节点,结果找不到下一个了。
  • 头节点指针被意外修改——比如在函数里改了头指针,但没通过二级指针传回。
// 错误示例:插入时顺序错误导致断链
// 想在prev之后插入newNode
newNode->next = prev->next;  // 正确:先让newNode指向后面的节点
prev->next = newNode;        // 正确:再让prev指向newNode
// 如果顺序反过来:
prev->next = newNode;        // 先改prev的next,后面的节点就丢了!
newNode->next = prev->next;  // 此时prev->next已经是newNode自己了,形成环

警告:插入和删除操作,一定要先处理“远端的指针”,再处理“近端的指针”。简单记:先接后断。先把新节点接进去,再断开旧连接。

26.5 知识体系总览

下面这张图,把四种常见错误的核心原因和后果总结了一下。你可以把它当作一个速查表,写链表代码时对照着看。

链表常见错误知识体系 链表常见错误 野指针 指向已释放/未分配内存 内存泄漏 malloc后未free 死循环 链表成环,遍历不终止 断链 指针顺序错误,节点丢失 核心原则:先接后断,配对malloc/free,及时置NULL

26.6 避坑指南:我的实战经验

说了这么多,最后分享几个我自己的习惯,希望能帮你少走弯路。

  • 每次malloc后,立即检查返回值。如果返回NULL,别硬着头皮往下走。嵌入式设备内存紧张,分配失败是常有的事。
  • 写链表操作函数时,先画图。我习惯在纸上画出指针的变化过程,确认无误再写代码。别嫌麻烦,这比调试省时间多了。
  • 多用断言。在关键操作前后加assert,比如assert(p != NULL)。调试版本里开着,发布版本关掉,不影响性能。
  • 我曾经犯过一个错:在中断服务函数里操作链表,没加锁。结果主循环和中断同时修改链表,直接断链。后来我规定:链表操作统一放在主循环里,中断只发信号。

最后说一句:链表调试最好的工具不是调试器,是你的脑子。每次操作前,把指针的指向在脑子里过一遍。时间长了,你自然就能“看见”内存里的链表长什么样。


公众号:蓝海资料掘金营,微信deep3321