第十三章 动态内存管理: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函数:申请并清零
calloc 和 malloc 很像,但它多做了两件事:
- 自动计算总字节数(你只需提供元素个数和每个元素的大小)
- 把申请到的内存全部初始化为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 更省事。但要注意,calloc 比 malloc 多了一个清零操作,性能上会稍慢一点。在嵌入式场景下,如果内存很大且不需要清零,用 malloc 更合适。
13.4 realloc函数:调整内存大小
有时候,申请的内存不够用了,或者太多了想缩小。这时候 realloc 就派上用场了。
原型:
void* realloc(void* ptr, size_t new_size);
它的行为有点复杂,我总结一下:
- 如果
ptr为NULL,行为等同于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);
使用要点:
- 只能释放由
malloc、calloc、realloc申请的内存 - 释放后,指针变成野指针,建议立即置为
NULL - 不能重复释放同一块内存(double free 会导致程序崩溃)
- 释放
NULL是安全的(什么也不做)
标准写法:
free(p);
p = NULL; // 避免野指针
13.6 内存泄漏的避免
内存泄漏,说白了就是申请了内存但忘了释放。程序跑得越久,泄漏越多,最后系统资源耗尽。
我曾经在一个通信协议栈项目里,发现一个函数每次调用都泄漏128字节。程序每秒钟调用这个函数100次,跑一天就泄漏了约1GB内存。服务器三天后必挂。
避免内存泄漏的几个原则:
- 谁申请,谁释放——函数内malloc的内存,尽量在函数内free
- 成对出现——写malloc的时候,立刻写下对应的free
- 使用工具检测——Valgrind、AddressSanitizer都是好帮手
- 封装成函数——把内存管理封装起来,统一入口和出口
避坑指南:我曾经在中断服务函数里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 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
13.9 总结
动态内存管理是C语言的精髓之一。掌握好malloc、calloc、realloc、free这四个函数,你就能写出灵活高效的程序。
记住几个关键点:
- 每次申请内存后检查返回值是否为NULL
- 释放内存后立即将指针置为NULL
- 不要重复释放同一块内存
- 动态数组的扩容策略用翻倍法,性能最优
嗯,这一章的内容就到这里。代码多写几遍,自然就熟练了。