4. 动态内存分配:malloc/calloc/realloc/free 的正确使用与配对

动态内存分配,说白了就是程序运行时跟操作系统“要内存”。

你写代码时不知道数组该开多大?运行时数据量会变?那就得靠动态分配。C语言里,malloc、calloc、realloc 和 free 这四位就是干这活的。

但我得说句实话——这四位用好了是神器,用不好就是定时炸弹。我在项目中见过太多因为动态内存搞出来的崩溃,有的甚至上线几个月才暴露出来。

4.1 malloc:最基础的分配器

malloc 负责从堆上申请一块连续内存。原型很简单:

void* malloc(size_t size);

它只做一件事:给你 size 字节的空间。不初始化,里面是垃圾值。

核心要点:

  • 返回 void*,需要强转成目标类型指针
  • 分配失败返回 NULL——这个一定要检查
  • 分配的内存内容不确定,必须手动初始化

举个例子:

int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
    // 处理分配失败
    fprintf(stderr, "内存分配失败\n");
    return -1;
}
// 使用前务必初始化
for (int i = 0; i < 10; i++) {
    arr[i] = 0;
}

我个人习惯,每次 malloc 后面紧跟 NULL 检查。别嫌麻烦,你想想看,一个空指针解引用,程序直接就崩了。

4.2 calloc:带初始化的分配

calloc 和 malloc 的区别就一点:它会把分配的内存全部清零。

void* calloc(size_t num, size_t size);

参数是两个:元素个数和每个元素的大小。总大小就是 num * size。

我的建议:

如果你需要初始化为零的内存,直接用 calloc 代替 malloc + memset。一来代码更简洁,二来 calloc 内部可能做了优化,性能不一定比手动清零差。

// 分配 100 个 int,全部初始化为 0
int* data = (int*)calloc(100, sizeof(int));
if (data == NULL) {
    // 处理失败
}

不过要注意,calloc 的清零是“按位清零”。对于指针类型,这意味所有指针都是 NULL,这倒是挺安全的。

4.3 realloc:动态调整大小

这是最容易被误用的一个。它的作用是调整已分配内存的大小。

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

逻辑是这样的:

  • 如果 ptr 是 NULL,行为等价于 malloc(new_size)
  • 如果 new_size 是 0,行为等价于 free(ptr)
  • 否则,尝试调整 ptr 指向的内存块到 new_size 字节

⚠️ 我曾经踩过的坑:

千万不要这样写:

ptr = realloc(ptr, new_size);  // 错误用法!

如果 realloc 失败,它会返回 NULL,但原来的内存块不会被释放。这时候 ptr 被覆盖成 NULL,原来的内存就泄漏了。

正确做法:

void* new_ptr = realloc(ptr, new_size);
if (new_ptr == NULL) {
    // realloc 失败,ptr 仍然有效
    free(ptr);
    return -1;
}
ptr = new_ptr;  // 成功后再赋值

4.4 free:释放的艺术

有借有还,再借不难。free 就是把借来的内存还回去。

void free(void* ptr);

这里有几个铁律:

情况 是否允许 后果
释放 NULL ✅ 安全 无操作
释放已释放的内存 ❌ 禁止 未定义行为(double free)
释放栈上变量 ❌ 禁止 堆损坏,程序崩溃
释放后继续使用 ❌ 禁止 use-after-free,安全漏洞

我的习惯:

每次 free 之后,立刻把指针置为 NULL:

free(ptr);
ptr = NULL;

这样即使不小心再次 free,也是安全的。虽然不能解决所有问题,但能避免大部分 double free 的尴尬。

4.5 配对原则:谁分配,谁释放

这是动态内存管理最核心的规则。说白了就是:

  • malloc 配 free
  • calloc 配 free
  • realloc 配 free

谁分配的内存,就由谁来释放。模块内部分配的内存,模块内部释放。库函数分配的内存,由库提供释放函数。

我在项目中见过最典型的问题:A 函数 malloc 了一块内存,B 函数 free 掉了,然后 A 函数还在用。这种跨模块的内存管理,最容易出问题。

避坑指南:

  • 每个 malloc/calloc 都要有对应的 free
  • 使用 realloc 时,用临时变量接收返回值
  • 释放后立即置 NULL
  • 不要释放非堆内存
  • 不要重复释放同一块内存

4.6 知识体系总览

下面这张图,把动态内存分配的核心逻辑串起来了:

动态内存分配核心流程 malloc(size) calloc(num, size) realloc(ptr, size) 检查返回值是否为 NULL? 是(失败) 否(成功) 处理错误 / 返回 正常使用内存 free(ptr) ptr = NULL(推荐)

这张图把整个流程串起来了:分配 → 检查 → 使用 → 释放 → 置空。每一步都不能省。

4.7 常见错误模式

最后,我总结几个最常见的错误,你在代码审查时可以重点盯着看:

  1. 内存泄漏:分配了没释放。程序跑久了内存越占越多,最后 OOM。
  2. 野指针:释放后没置 NULL,指针还在,但指向的内存已经还给系统了。
  3. 缓冲区溢出:分配了 10 个字节,写了 20 个。堆结构被破坏,崩溃是迟早的事。
  4. 重复释放:两个地方都调了 free,堆管理器的链表被搞乱。
  5. 忘记检查 NULL:分配失败直接使用,空指针解引用,段错误。

⚠️ 我曾经遇到的一个真实案例:

一个网络服务程序,上线运行了三个月,突然开始随机崩溃。查到最后,是一个回调函数里 malloc 了内存,但只在正常路径下 free,异常路径直接 return 了。每次异常泄漏 64 字节,三个月下来堆被撑爆了。

从那以后,我写代码都会在脑子里过一遍所有执行路径,确保每条路径上分配和释放都成对出现。

嗯,动态内存管理就是这样——规则不多,但每条都是血的教训换来的。记住配对原则,养成好习惯,能省去你无数调试的夜晚。

一句话总结:

谁分配,谁释放;释放后,即置空;用之前,判空否。

可可精品资料库