4、常见内存泄露场景:忘记释放内存、多次释放、野指针、循环引用

内存泄露这玩意儿,说大不大,说小不小。我见过太多IVI系统死机、卡顿,最后查下来就是内存泄露在作祟。说白了,就是申请了内存,用完了没还回去。今天咱们就把最常见的四种场景掰开揉碎了讲清楚。

4.1 忘记释放内存

这是最原始、也最容易犯的错误。你申请了一块堆内存,用完之后忘了调用free()。一次两次没事,跑个几天几夜,系统就扛不住了。

我举个例子,你在车载导航里加载地图POI数据:

// 错误示例:忘记释放
void load_poi_data(void) {
    char *buffer = (char *)malloc(1024 * 1024);  // 申请1MB
    // ... 读取数据,处理 ...
    // 忘了 free(buffer)!
}

每次调用这个函数,就漏掉1MB。导航开个长途,几百次调用下来,内存就被吃光了。

核心原则:malloc 和 free 必须成对出现。申请一次,释放一次,别偷懒。

我个人习惯是,在写malloc的时候,立马把free写上去。就像这样:

// 推荐做法:先写释放,再写申请
void load_poi_data(void) {
    char *buffer = (char *)malloc(1024 * 1024);
    if (!buffer) return;
    
    // ... 处理数据 ...
    
    free(buffer);  // 写malloc的同时就写好free
    buffer = NULL;
}

嗯,这里要注意,释放后置空是个好习惯。防止后面不小心又用到这块地址。

4.2 多次释放

这个坑我踩过。你释放了一次内存,后来又因为某个条件判断,又释放了一次。double free 会导致堆结构损坏,程序直接崩溃。

为什么会这样?因为第一次free后,那块内存已经被归还给堆管理器了。你再free一次,堆管理器就懵了——这块地儿我还没登记呢!

// 错误示例:多次释放
void process_data(void) {
    int *data = (int *)malloc(100 * sizeof(int));
    // ... 使用 data ...
    
    free(data);
    
    // 某个错误处理分支
    if (some_error) {
        free(data);  // 二次释放!崩溃!
    }
}

避坑指南:我曾经在一个音频解码模块里,因为多个回调函数都调用了同一个释放逻辑,导致double free。排查了两天才找到。从那以后,我强制要求:释放后立即置NULL,并且释放前检查是否为NULL。

// 安全做法:释放前检查,释放后置空
if (data) {
    free(data);
    data = NULL;
}

4.3 野指针

野指针,就是指向了非法内存地址的指针。它比空指针更危险。空指针你还能判断,野指针你根本不知道它指向哪里。

野指针通常有三种来源:

  • 指针未初始化:局部指针变量默认值是随机的
  • 释放后未置空:free之后指针还保留着原来的地址
  • 返回栈地址:函数返回了局部变量的地址
// 野指针示例
int* get_value(void) {
    int local = 42;
    return &local;  // 返回栈地址!函数返回后这块内存就失效了
}

void use_pointer(void) {
    int *p = get_value();
    printf("%d\n", *p);  // 野指针!可能打印出垃圾值,也可能崩溃
}

你想想看,在IVI系统里,这种野指针一旦访问到关键数据区,轻则花屏,重则系统重启。我在项目中遇到过,一个触摸事件处理函数返回了栈上变量的地址,结果每次点击屏幕,系统就随机崩溃一次。

我的建议:所有指针定义时都初始化为NULL。释放后也置NULL。函数返回指针时,确保指向的是堆内存或全局区,绝不是栈区。

4.4 循环引用

循环引用在C语言里,通常出现在双向链表、树结构或者引用计数的场景中。两个对象互相持有对方的指针,导致谁都无法被释放。

举个例子,IVI系统中的消息订阅机制:

// 循环引用示例
typedef struct node {
    int id;
    struct node *next;
    struct node *prev;  // 双向链表
} node_t;

void create_cycle(void) {
    node_t *a = (node_t *)malloc(sizeof(node_t));
    node_t *b = (node_t *)malloc(sizeof(node_t));
    
    a->next = b;
    b->prev = a;  // 互相引用
    
    // 释放时只释放了a和b本身
    free(a);
    free(b);
    // 但a->next和b->prev还指向对方?其实这里没问题
    // 真正的循环引用问题在引用计数场景更明显
}

上面这个例子其实还好,因为直接free了。真正头疼的是引用计数场景:

// 引用计数循环引用
typedef struct object {
    int ref_count;
    struct object *friend;  // 指向另一个对象
} object_t;

void ref_cycle(void) {
    object_t *obj1 = create_object();
    object_t *obj2 = create_object();
    
    obj1->friend = obj2;
    obj2->friend = obj1;  // 循环引用!
    
    // 即使外部不再引用obj1和obj2
    // 它们内部的ref_count永远不会降到0
    // 内存永远无法释放
}

关键点:循环引用导致引用计数永远不为零。说白了,就是两个对象互相抱着,谁都不肯松手。垃圾回收器也拿它们没办法。

怎么解决?我一般用两种方法:

  • 弱引用:其中一个方向使用普通指针,不增加引用计数
  • 手动打破循环:在适当时机,主动将其中一个引用置NULL

我曾经在开发车载蓝牙电话模块时,遇到一个通话记录列表的内存泄露。查了三天,最后发现是联系人对象和通话记录对象互相引用,导致整个链表都释放不掉。后来改成单向引用,问题就解决了。

场景 典型表现 排查难度 推荐工具
忘记释放 内存持续增长 Valgrind
多次释放 随机崩溃 AddressSanitizer
野指针 花屏、死机 GDB + 堆栈分析
循环引用 内存缓慢泄露 代码审查 + 引用计数日志

最后说一句,内存泄露的排查,说白了就是耐心活。别指望一次就能找到。我每次遇到内存问题,都会先怀疑自己写的代码,再怀疑第三方库。嗯,这个习惯帮我省了不少时间。