1. 内存泄漏基础:什么是内存泄漏、内存泄漏的危害、常见的内存泄漏场景

1.1 什么是内存泄漏?

说白了,内存泄漏就是「你借了东西不还」。

在C/C++里,我们用 malloc、new 向系统申请了一块内存,用完之后却没有用 free、delete 还回去。这块内存就「丢」了——程序再也找不到它,系统也没法回收它。

我习惯这么理解:内存泄漏 = 申请 + 丢失 + 永不释放。

举个例子:

void leak_example() {
    int *p = (int*)malloc(100 * sizeof(int));
    // 用了一下 p
    // 但忘了 free(p)
    return;  // p 丢了!100个int的内存永远回不来了
}

你看,函数返回后,指针 p 没了,但那块内存还在。程序再也找不到它了。这就是泄漏。

核心定义:内存泄漏是指程序中已动态分配的堆内存,由于某种原因未被释放,导致该内存无法被程序后续使用,也无法被操作系统回收。

1.2 内存泄漏的危害

你可能觉得:「不就漏了一点内存吗?能有多大问题?」

嗯,我刚开始做嵌入式开发时也这么想。直到有一次,我们的设备在客户现场跑了三天,突然挂了。查了整整一周,发现是一个循环里每次漏了 64 字节,三天下来累积了几百兆,直接把堆耗尽了。

内存泄漏的危害,我总结为四点:

危害 说明 真实场景
程序崩溃 堆耗尽后,malloc/new 返回 NULL,程序直接挂 服务器运行几天后突然宕机
性能下降 内存碎片增多,分配速度变慢 系统越来越卡,响应延迟飙升
系统不稳定 占用系统内存,影响其他进程 嵌入式设备死机、重启
难以排查 泄漏往往在特定路径下才触发 测试时正常,上线后出问题
注意:内存泄漏是「慢性病」。它不会立刻杀死你的程序,但会在你最不想出问题的时候——比如凌晨三点、客户演示现场——突然爆发。

1.3 常见的内存泄漏场景

我在项目中遇到过各种各样的泄漏。说白了,大部分泄漏都是「人祸」——要么是忘了释放,要么是释放错了地方。下面这几种场景,你十有八九也碰到过。

场景一:忘记释放

最基础的,也是最常见的。申请了内存,但释放代码被跳过了。

void process_data(int flag) {
    char *buf = (char*)malloc(1024);
    if (flag == 0) {
        return;  // 忘了 free(buf)!
    }
    // 正常处理...
    free(buf);
}

你看,flag == 0 时直接 return 了,free 根本没机会执行。这种错误,我建议你在写代码时就养成习惯:申请内存后,立刻写对应的释放代码。

场景二:释放不完全

这个坑我踩过好几次。比如你有一个结构体,里面嵌套了指针:

typedef struct {
    char *name;
    int   age;
} Person;

Person *create_person() {
    Person *p = (Person*)malloc(sizeof(Person));
    p->name = (char*)malloc(64);  // 内部又申请了内存
    return p;
}

void destroy_person(Person *p) {
    free(p);  // 只释放了 Person 本身
    // 但 p->name 没释放!泄漏了!
}

你想想看,free(p) 只释放了外层结构体,内部的 name 指针指向的那块内存还在。这就是「释放不完全」。

我的习惯:写释放函数时,遵循「先内后外」的原则——先释放内部指针,再释放外层结构体。顺序反了,内部指针就找不到了。

场景三:异常路径泄漏

这是最隐蔽的一种。程序正常跑没问题,但一旦遇到错误、异常、提前返回,泄漏就出现了。

int read_file(const char *path) {
    FILE *fp = fopen(path, "r");
    char *buf = (char*)malloc(4096);
    
    if (fp == NULL) {
        return -1;  // 泄漏了 buf!
    }
    
    if (fread(buf, 1, 4096, fp) < 0) {
        fclose(fp);
        return -2;  // 又泄漏了 buf!
    }
    
    // 正常处理...
    free(buf);
    fclose(fp);
    return 0;
}

我曾经在一个网络协议栈里排查过一个类似的泄漏。正常收发数据没问题,但一旦网络断开、重连,每次都会漏掉几百字节。跑了两个月,内存占用从 2MB 涨到了 800MB。

场景四:容器中的泄漏

用 STL 容器时,如果存的是指针,很容易出问题:

std::vector<int*> vec;
for (int i = 0; i < 100; i++) {
    vec.push_back(new int(i));
}
// 用完了...
vec.clear();  // 只清空了指针,没释放指向的内存!
// 100个int全泄漏了

你想想看,clear() 只是把容器里的指针清掉了,但 new 出来的那些 int 还在堆上。正确的做法是遍历释放,或者直接用 unique_ptr。

1.4 知识体系总览

下面这张图,是我梳理的内存泄漏知识体系。你可以把它当作整个课程的地图:

内存泄漏知识体系 内存泄漏 什么是内存泄漏 内存泄漏的危害 常见泄漏场景 申请后未释放 指针丢失 程序崩溃 性能下降 忘记释放 释放不完全 异常路径泄漏 容器中泄漏 排查技巧:Valgrind / AddressSanitizer / 代码审查

1.5 小结

这一章我们聊了内存泄漏的「是什么、为什么、怎么来」。说白了,内存泄漏就是「借了不还」,它像慢性毒药一样慢慢侵蚀你的程序。

我个人觉得,理解泄漏的本质比学会排查工具更重要。因为只有你知道「泄漏是怎么发生的」,才能在写代码时就把它扼杀在摇篮里。

嗯,下一章我们开始动手——我会带你用 Valgrind 抓出第一个泄漏。不过在那之前,建议你把今天讲的几种场景在自己的代码里找一找,说不定就有惊喜(或者惊吓)。

一句话记住:每次 malloc/new,都要问自己一句——这个内存,我打算在哪里 free/delete?
可可精品资料库