第22章:动态内存管理——malloc、calloc、realloc、free的使用,内存泄漏的避免

说实话,动态内存管理是C语言里最容易翻车的地方之一。我见过太多项目,明明逻辑没问题,跑着跑着就崩了——查到最后,十有八九是内存出了问题。

你想想看,静态数组的大小是编译时就定死的。但实际开发中,你往往要到运行时才知道需要多少内存。比如你要读一个文件,文件多大?不知道。这时候就需要动态内存管理了。

嗯,这一章我们就把它彻底讲透。

一、四个核心函数

C语言标准库提供了四个函数来管理堆内存。它们都声明在 <stdlib.h> 里。

1. malloc —— 申请一块内存

void *malloc(size_t size);

malloc 分配 size 字节的内存。它不会初始化这块内存——里面的数据是随机的。我个人习惯,拿到 malloc 返回的指针后,如果马上要用,就手动 memset 清零。

关键点:malloc 返回的是 void*,需要强制转换成目标类型。分配失败返回 NULL。

int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理分配失败
}

2. calloc —— 申请并清零

void *calloc(size_t nmemb, size_t size);

calloc 分配 nmemb 个元素,每个元素 size 字节。它会把分配的内存全部初始化为 0。

我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,结果某个成员没初始化,导致后续逻辑判断出错。后来改用 calloc,问题就解决了。

int *p = (int *)calloc(10, sizeof(int));
// 这10个int全部是0

小技巧:如果你需要清零的内存,直接用 calloc 比 malloc + memset 更高效。因为 calloc 在底层可能利用操作系统的零页优化。

3. realloc —— 调整内存大小

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

realloc 用来调整之前分配的内存块大小。它可能原地扩展,也可能搬到一个新地址。

这里有个坑,我曾经踩过:

// 错误用法
int *p = (int *)malloc(10 * sizeof(int));
p = (int *)realloc(p, 20 * sizeof(int));  // 如果realloc失败,返回NULL,原来的内存就丢了!

正确的做法是:

int *p = (int *)malloc(10 * sizeof(int));
int *tmp = (int *)realloc(p, 20 * sizeof(int));
if (tmp != NULL) {
    p = tmp;  // 只有成功才赋值
} else {
    // 处理失败,p仍然有效
}

4. free —— 释放内存

void free(void *ptr);

free 把内存还给系统。注意:只能释放 malloc/calloc/realloc 返回的指针。不能释放栈上的地址,也不能重复释放。

警告:free 之后一定要把指针置为 NULL。否则就成了野指针,后续误用会出大问题。

free(p);
p = NULL;  // 好习惯

二、内存泄漏——看不见的杀手

内存泄漏是什么?说白了就是你申请了内存,用完了没释放。程序跑得越久,占用的内存越多,最后系统扛不住了。

我见过最离谱的一次:一个嵌入式设备跑了三天后死机,查了整整一周才发现是一个循环里每次都要 malloc,但只在特定条件下才 free。条件不满足时,内存就漏了。

常见泄漏场景

  • 忘记 free:最直接的泄漏
  • 丢失指针:把指针重新赋值了,原来的内存再也找不回来
  • 异常路径没释放:函数中间 return 了,free 代码没执行到
  • 结构体嵌套:释放了外层,忘了释放内层指针指向的内存

三、避坑指南

这些都是我用真金白银换来的教训:

我曾经...在一个通信协议栈里,为了省事,把 malloc 和 free 分散在不同的模块里。结果调试了整整两天,才发现是某个模块在释放后没有置 NULL,另一个模块又去访问了。

从那以后,我给自己定了几个规矩:

  1. 谁申请谁释放——尽量让同一个函数或模块负责 malloc 和 free
  2. free 后立即置 NULL——这是肌肉记忆
  3. 检查返回值——malloc/calloc/realloc 都可能失败,尤其是嵌入式环境
  4. 用工具检测——Valgrind 是 Linux 下的神器,嵌入式可以用静态分析工具

四、知识体系图

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

动态内存管理核心流程 申请内存 malloc(size) 不初始化,内容随机 calloc(n, size) 自动清零 realloc(ptr, size) 调整大小,可能搬移 使用内存 free(ptr) ⚠️ 常见陷阱 • 忘记 free → 内存泄漏 • 重复 free → 崩溃 • 野指针 → 未定义行为 • realloc 失败不检查 ✅ 最佳实践 • 谁申请谁释放 • free 后置 NULL • 检查返回值 • 用 Valgrind 检测

五、完整示例

最后,给你看一个实际项目中常用的模式——动态数组:

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} DynamicArray;

// 初始化
int da_init(DynamicArray *arr, size_t cap) {
    arr->data = (int *)malloc(cap * sizeof(int));
    if (arr->data == NULL) return -1;
    arr->size = 0;
    arr->capacity = cap;
    return 0;
}

// 添加元素
int da_push(DynamicArray *arr, int val) {
    if (arr->size >= arr->capacity) {
        // 扩容:每次翻倍
        size_t new_cap = arr->capacity * 2;
        int *tmp = (int *)realloc(arr->data, new_cap * sizeof(int));
        if (tmp == NULL) return -1;
        arr->data = tmp;
        arr->capacity = new_cap;
    }
    arr->data[arr->size++] = val;
    return 0;
}

// 释放
void da_free(DynamicArray *arr) {
    free(arr->data);
    arr->data = NULL;
    arr->size = 0;
    arr->capacity = 0;
}

int main() {
    DynamicArray arr;
    if (da_init(&arr, 4) != 0) {
        printf("初始化失败\n");
        return 1;
    }
    
    for (int i = 0; i < 10; i++) {
        da_push(&arr, i * i);
    }
    
    for (size_t i = 0; i < arr.size; i++) {
        printf("%d ", arr.data[i]);
    }
    printf("\n");
    
    da_free(&arr);
    return 0;
}

这个模式我用了很多年。它把内存管理的细节封装在几个函数里,外面调用的人不用操心 malloc 和 free 的细节。说白了,就是降低心智负担。

最后说一句:动态内存管理没有银弹。你只能靠良好的编码习惯和工具辅助来减少问题。我个人的经验是——写代码时多花一分钟想清楚释放路径,能省下后面十小时的调试时间。


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