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 指向的那块内存,从此成了孤儿。每次调用这个函数,就丢一块内存。跑个几千次,内存占用就上去了。

⚠️ 注意: 这不是语法错误,编译器不会报错。这是典型的逻辑漏洞,只有跑起来、跑久了才会暴露。

二、正确的删除操作:三步走

我个人习惯,写链表删除时脑子里过三个步骤:

  1. 找到节点 —— 遍历链表,定位目标
  2. 摘除节点 —— 调整前后指针,让目标脱离链表
  3. 释放内存 —— 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 之后,指针就是废纸一张。别再用它,也别让别人再用它。

五、避坑指南:我踩过的那些坑

我曾经在一个嵌入式项目里,用链表管理动态分配的传感器数据包。删除节点时忘了 free(),跑了三天,内存从 64KB 涨到 60MB,系统直接挂了。从那以后,我给自己定了几个规矩:

  • 写删除函数时,先写 free,再写指针调整 —— 顺序反过来容易忘
  • 每个 malloc 都要有对应的 free —— 写代码时就配对,别等调试时再补
  • 删除后把指针置 NULL —— 虽然不能完全避免问题,但能提前暴露 bug
  • 用 valgrind 或 AddressSanitizer 跑一遍 —— 工具能帮你找到肉眼漏掉的问题

六、知识体系图

下面这张图,帮你理清链表删除时内存管理的核心逻辑:

链表节点删除:内存管理核心流程 ① 定位目标节点 遍历链表,找到待删节点 ② 摘除节点 调整前后指针,断开链接 ③ 释放内存 free() 归还系统 ⚠️ 常见错误分支 错误1:忘记 free() 节点从链表摘除,但内存未释放 → 内存泄漏,长期运行必挂 错误2:free 后继续使用 访问已释放的内存 → 未定义行为,可能段错误 ✅ 正确做法:三步缺一不可 + free 后指针置 NULL 工具辅助:valgrind / AddressSanitizer 定期检查

七、总结

链表删除的内存泄漏,说白了就一句话:摘了节点不 free,等于把垃圾留在屋里。你想想看,嵌入式系统内存本来就金贵,一个节点几十字节,泄漏几千次就是几十 MB。在资源受限的环境下,这直接导致系统崩溃。

我个人习惯,每次写完链表操作,都会用 valgrind 跑一遍。没跑过内存检查的链表代码,我是不敢往项目里放的。嗯,这习惯救过我很多次。

📌 记住: malloc 和 free 是夫妻,拆散一对,就多一个内存孤儿。

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