第1章 动态内存管理:malloc/calloc/realloc/free、内存泄漏与野指针、常见内存错误

大家好,我是你们的老朋友。今天咱们来聊聊C语言里一个绕不开的话题——动态内存管理。说实话,我刚开始学C的时候,觉得这玩意儿挺简单的,不就是申请内存、用完了释放嘛。直到我在实际项目中栽了几个跟头,才真正明白这里面的门道有多深。

动态内存管理,说白了就是程序在运行的时候,跟操作系统要一块内存来用。用完了再还回去。你想想看,如果程序一开始就把所有内存都规划好,那得多死板?很多场景下,我们根本不知道用户会输入多少数据,或者程序运行过程中会产生多少中间结果。这时候,动态内存管理就派上用场了。

1.1 malloc:最基础的内存申请

malloc 是动态内存管理的入门函数。它的原型很简单:

void *malloc(size_t size);

它向系统申请 size 字节的连续内存空间,返回一个指向这块内存的指针。如果申请失败,返回 NULL

我个人习惯在使用 malloc 时,总是检查返回值。别嫌麻烦,这能救你一命。我曾经在一个嵌入式项目中,因为忽略了 malloc 返回 NULL 的情况,导致程序在内存不足时直接崩溃,现场调试了半天才找到原因。

核心要点:

  • malloc 分配的内存是未初始化的,里面是随机值
  • 返回类型是 void*,需要强制类型转换到你需要的指针类型
  • 分配失败返回 NULL,一定要检查
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理内存分配失败
    fprintf(stderr, "内存分配失败\n");
    exit(1);
}
// 现在 p 指向一块可以存放 10 个 int 的内存

1.2 calloc:带初始化的内存申请

callocmalloc 很像,但有两个区别:第一,它接受两个参数;第二,它会将分配的内存全部初始化为 0。

void *calloc(size_t nmemb, size_t size);

nmemb 是元素个数,size 是每个元素的大小。总分配大小就是 nmemb * size

嗯,这里要注意:calloc 的初始化是逐字节置 0,对于指针类型来说,就是 NULL。这在某些场景下非常有用。我记得有一次写一个哈希表,用 calloc 分配桶数组,省去了手动初始化的步骤,代码干净了不少。

小技巧:如果你需要分配的内存后续要全部覆盖写入,用 malloc 就行,省掉初始化开销。如果内存内容不确定,或者需要默认全零,用 calloc 更安全。

1.3 realloc:动态调整内存大小

程序跑着跑着,发现之前分配的内存不够用了,怎么办?realloc 就是干这个的。

void *realloc(void *ptr, size_t new_size);

它尝试将 ptr 指向的内存块调整为 new_size 字节。如果调整成功,返回新的内存地址(可能和原来一样,也可能不一样)。如果失败,返回 NULL,原来的内存块保持不变。

重要警告:千万不要这样写:

p = realloc(p, new_size);  // 错误!如果 realloc 失败,p 变成 NULL,原内存泄漏

正确做法是先用临时变量接收返回值:

int *new_p = realloc(p, new_size);
if (new_p == NULL) {
    // 处理失败,p 仍然有效
} else {
    p = new_p;
}

我曾经在代码审查时看到过这种错误,当时就提醒了同事。他还不信,直到我给他演示了内存泄漏的场景……嗯,从那以后他再也不敢这么写了。

1.4 free:归还内存

有借有还,再借不难。free 就是用来归还动态内存的。

void free(void *ptr);

调用 free 后,ptr 指向的内存被释放,可以被系统重新分配。但 ptr 本身的值并没有改变,它仍然指向那块已经被释放的内存——这就是野指针的根源之一。

最佳实践:

  • 释放后立即将指针置为 NULL
  • 不要重复释放同一块内存
  • 不要释放栈上的内存(非 malloc/calloc/realloc 返回的指针)
free(p);
p = NULL;  // 避免野指针

1.5 内存泄漏:看不见的杀手

内存泄漏,说白了就是你申请了内存,但忘了释放。程序跑得越久,泄漏的内存越多,最终可能导致系统资源耗尽。

我参与过一个长期运行的服务器项目,刚开始测试一切正常,但跑了三天后,内存占用从 100MB 涨到了 2GB。查了半天,发现是一个循环里每次迭代都 malloc 了一小块内存,但只在特定条件下才 free。条件不满足时,内存就泄漏了。

避坑指南:我曾经用过一个简单的方法来追踪内存泄漏——在 mallocfree 的地方加日志,记录分配和释放的地址。程序退出时,检查哪些地址只分配没释放。虽然土,但很有效。

1.6 野指针:程序崩溃的元凶

野指针是指向无效内存的指针。常见的产生原因有:

  • 指针未初始化
  • 指针指向的内存已被释放
  • 指针越界访问

野指针的危害在于,它可能不会立即导致程序崩溃,而是在某个不确定的时刻突然出问题。这种 bug 最难排查。

int *p;  // 未初始化,p 是野指针
*p = 10; // 危险!不知道写到哪里去了

血的教训:我曾经在一个项目中,因为野指针导致数据被意外修改,排查了整整两天。最后发现是一个全局指针在某个函数里被 free 了,但其他函数还在使用它。从那以后,我养成了释放后立即置 NULL 的习惯。

1.7 常见内存错误一览

错误类型 表现 后果
内存泄漏 申请后未释放 内存占用持续增长,最终耗尽
野指针 指向已释放或未初始化的内存 程序崩溃或数据损坏
重复释放 对同一指针多次调用 free 堆损坏,程序崩溃
越界访问 读写超出分配范围 破坏相邻内存,引发奇怪 bug
释放栈内存 对非堆指针调用 free 未定义行为,通常崩溃

1.8 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

动态内存管理知识体系 动态内存管理 malloc calloc realloc free 内存泄漏 野指针 常见内存错误 重复释放 越界访问

1.9 总结与建议

动态内存管理是C语言中容易出错的地方,但也是体现程序员功底的地方。我个人总结了三条黄金法则:

  1. 谁申请,谁释放——明确每一块内存的归属,避免责任不清
  2. 释放后置 NULL——从根源上杜绝野指针
  3. 检查返回值——永远假设 malloc 可能失败

记住这三条,能帮你避开 90% 的内存问题。剩下的 10%,嗯,那就靠经验和调试工具了。我在实际项目中用过 Valgrind 和 AddressSanitizer,效果都不错,建议你也试试。

好了,这一章就到这里。动态内存管理是基础中的基础,但也是很多 bug 的温床。希望你能把这些概念真正吃透,而不是停留在「好像懂了」的层面。下次见!


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