第十三章 动态内存管理:malloc、calloc、realloc、free与内存泄漏

说实话,C语言里最让初学者头疼的,就是内存管理了。

我记得刚入行那会儿,写了一个数据处理模块,跑起来没问题,但运行三天后突然崩溃。查了两天,最后发现是内存泄漏——申请的内存没释放,系统资源被吃光了。从那以后,我对动态内存管理就格外上心。

这一章,咱们就把malloc、calloc、realloc、free这几个函数彻底讲透。你想想看,掌握了它们,你就能在程序运行时灵活地申请和释放内存,这才是真正的「动态」编程。

13.1 为什么需要动态内存分配?

静态数组有个硬伤:大小必须在编译时确定。比如你写 int arr[100],那它永远只能存100个整数。如果用户需要存200个呢?要么浪费空间,要么程序崩溃。

动态内存分配解决了这个问题。程序运行时,你可以根据实际需要申请内存,用完了再还回去。说白了,就是「按需分配,用完即还」。

核心概念:动态内存分配发生在堆(heap)区,而不是栈(stack)区。堆区的生命周期由程序员控制,栈区由编译器自动管理。

13.2 malloc函数:申请内存

malloc 是最常用的内存申请函数。它的原型是:

void* malloc(size_t size);

参数 size 是你想申请的字节数。返回值是一个 void* 指针,指向申请到的内存首地址。如果申请失败,返回 NULL

举个例子,申请一个能存10个整数的空间:

int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理内存申请失败
    printf("内存申请失败!\n");
    return -1;
}

我的习惯:每次malloc后立刻检查返回值是否为NULL。别偷懒,嵌入式设备内存有限,申请失败是常有的事。

注意,malloc 申请的内存是未初始化的。里面存的是随机值,直接使用会有风险。我曾经在项目中遇到过,有人malloc后没初始化就直接用,结果数据全是乱码,排查了半天。

13.3 calloc函数:申请并清零

callocmalloc 很像,但它多做了两件事:

  • 自动计算总字节数(你只需提供元素个数和每个元素的大小)
  • 把申请到的内存全部初始化为0

原型:

void* calloc(size_t num, size_t size);

示例:

int* p = (int*)calloc(10, sizeof(int));
// 等价于 malloc(10 * sizeof(int)) 然后 memset(p, 0, 10 * sizeof(int));

我个人建议,如果你需要清零的内存,直接用 calloc 更省事。但要注意,callocmalloc 多了一个清零操作,性能上会稍慢一点。在嵌入式场景下,如果内存很大且不需要清零,用 malloc 更合适。

13.4 realloc函数:调整内存大小

有时候,申请的内存不够用了,或者太多了想缩小。这时候 realloc 就派上用场了。

原型:

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

它的行为有点复杂,我总结一下:

  • 如果 ptrNULL,行为等同于 malloc(new_size)
  • 如果 new_size 为0,行为等同于 free(ptr)
  • 如果新大小比原来大,会尝试扩展内存块。如果原位置后面空间不够,会重新找一块连续空间,把旧数据拷贝过去,然后释放旧内存
  • 如果新大小比原来小,可能会截断数据(后面的数据丢失)

示例:

int* p = (int*)malloc(5 * sizeof(int));
// ... 使用中 ...
int* new_p = (int*)realloc(p, 10 * sizeof(int));
if (new_p == NULL) {
    // realloc失败,原内存p仍然有效
    printf("扩展内存失败!\n");
} else {
    p = new_p;  // 更新指针
}

注意:realloc返回的新指针可能和旧指针不同。千万不要直接用原指针,否则如果realloc移动了内存,原指针就变成野指针了。

13.5 free函数:释放内存

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

原型:

void free(void* ptr);

使用要点:

  • 只能释放由 malloccallocrealloc 申请的内存
  • 释放后,指针变成野指针,建议立即置为 NULL
  • 不能重复释放同一块内存(double free 会导致程序崩溃)
  • 释放 NULL 是安全的(什么也不做)

标准写法:

free(p);
p = NULL;  // 避免野指针

13.6 内存泄漏的避免

内存泄漏,说白了就是申请了内存但忘了释放。程序跑得越久,泄漏越多,最后系统资源耗尽。

我曾经在一个通信协议栈项目里,发现一个函数每次调用都泄漏128字节。程序每秒钟调用这个函数100次,跑一天就泄漏了约1GB内存。服务器三天后必挂。

避免内存泄漏的几个原则:

  1. 谁申请,谁释放——函数内malloc的内存,尽量在函数内free
  2. 成对出现——写malloc的时候,立刻写下对应的free
  3. 使用工具检测——Valgrind、AddressSanitizer都是好帮手
  4. 封装成函数——把内存管理封装起来,统一入口和出口

避坑指南:我曾经在中断服务函数里malloc内存,结果导致系统死锁。记住,中断里尽量不要做动态内存分配,因为malloc不是可重入的。

13.7 动态数组的实现

动态数组是动态内存管理最经典的应用。咱们来实现一个简单的动态数组,支持添加元素和自动扩容。

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

typedef struct {
    int* data;
    int size;      // 当前元素个数
    int capacity;  // 当前容量
} DynamicArray;

// 初始化
DynamicArray* da_create(int initial_capacity) {
    DynamicArray* arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    if (arr == NULL) return NULL;
    
    arr->data = (int*)malloc(initial_capacity * sizeof(int));
    if (arr->data == NULL) {
        free(arr);
        return NULL;
    }
    
    arr->size = 0;
    arr->capacity = initial_capacity;
    return arr;
}

// 添加元素(自动扩容)
int da_append(DynamicArray* arr, int value) {
    if (arr->size >= arr->capacity) {
        // 容量翻倍
        int new_capacity = arr->capacity * 2;
        int* new_data = (int*)realloc(arr->data, new_capacity * sizeof(int));
        if (new_data == NULL) return -1;  // 扩容失败
        
        arr->data = new_data;
        arr->capacity = new_capacity;
    }
    
    arr->data[arr->size] = value;
    arr->size++;
    return 0;
}

// 释放
void da_destroy(DynamicArray* arr) {
    if (arr == NULL) return;
    free(arr->data);
    free(arr);
    // 注意:调用后外部指针应置为NULL
}

这个实现里,每次容量不够时翻倍扩容。为什么是翻倍?因为这样平均每次插入的时间复杂度是O(1)。如果每次只增加1个空间,那插入n个元素就是O(n²),性能太差了。

13.8 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

动态内存管理核心知识体系 动态内存管理 malloc 申请未初始化内存 calloc 申请并清零内存 realloc 调整已申请内存大小 free 释放内存 常见问题与最佳实践 内存泄漏 申请后忘记释放 野指针 释放后未置NULL 动态数组 自动扩容的典型应用 核心原则:谁申请谁释放,成对出现,检查返回值

13.9 总结

动态内存管理是C语言的精髓之一。掌握好malloc、calloc、realloc、free这四个函数,你就能写出灵活高效的程序。

记住几个关键点:

  • 每次申请内存后检查返回值是否为NULL
  • 释放内存后立即将指针置为NULL
  • 不要重复释放同一块内存
  • 动态数组的扩容策略用翻倍法,性能最优

嗯,这一章的内容就到这里。代码多写几遍,自然就熟练了。


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