2. C语言内存管理基础
内存管理,说白了就是跟操作系统「借」和「还」的过程。我刚开始学C的时候,总觉得这玩意儿挺玄乎——不就是申请个空间吗?后来踩的坑多了,才明白这里面的门道有多深。
今天咱们就把这块彻底捋清楚。你想想看,一个程序跑起来,数据放哪儿?怎么放?放多久?这些问题搞不明白,内存泄漏迟早找上门。
2.1 堆与栈:两个不同的世界
先说说堆和栈。很多新手搞混这两个概念,其实它们完全是两码事。
栈(Stack):由编译器自动管理。你定义的局部变量、函数参数,都放在栈上。特点是:自动分配、自动释放。函数一结束,栈上的东西就没了。
堆(Heap):由程序员手动管理。你用 malloc 申请的内存,就在堆上。特点是:手动分配、手动释放。你不 free,它就一直在那儿。
我举个例子你就明白了:
void func() {
int a = 10; // a 在栈上
int *p = malloc(sizeof(int)); // p 在栈上,但指向堆内存
*p = 20;
// 函数结束,a 自动释放,但 p 指向的堆内存还在
// 如果不 free(p),就泄漏了
}
嗯,这里要注意:栈空间有限,一般就几MB。堆空间大得多,但管理起来也麻烦。我在项目中遇到过有人把大数组定义在栈上,结果栈溢出,程序直接崩了。从那以后,大块数据我都用堆。
核心区别总结:
- 栈:自动管理,速度快,空间小
- 堆:手动管理,速度慢,空间大
- 栈上变量生命周期随函数,堆上变量生命周期由你控制
2.2 malloc/free:最基础的内存操作
malloc 和 free 是 C 语言内存管理的基石。我个人习惯把它们看作「借」和「还」。
// 申请一个 int 大小的空间
int *p = (int *)malloc(sizeof(int));
if (p == NULL) {
// 处理内存分配失败
return -1;
}
*p = 42;
// 使用完后释放
free(p);
p = NULL; // 好习惯:释放后置空
这里有几个关键点:
- malloc 返回 void*,需要强制类型转换。虽然 C 语言允许隐式转换,但我建议显式写出来,代码更清晰。
- 检查返回值:malloc 可能失败,返回 NULL。不检查就直接用,程序会崩溃。我曾经在嵌入式项目里吃过这个亏——内存不够时 malloc 返回 NULL,我没检查,结果野指针访问,查了两天才找到原因。
- free 后置空:free 只是释放内存,指针的值还在。如果不置空,就成了悬空指针,后续误用会很危险。
避坑指南:
我曾经犯过一个经典错误——重复 free。第一次 free 后没置空,第二次又 free 了一次,程序直接 double free 崩溃。所以我现在养成习惯:free 之后立刻把指针置为 NULL。
2.3 calloc/realloc:更灵活的内存管理
除了 malloc,C 标准库还提供了 calloc 和 realloc。它们各有各的用处。
calloc:申请并初始化为 0
// 申请 10 个 int,每个初始化为 0
int *arr = (int *)calloc(10, sizeof(int));
// 等价于 malloc + memset,但 calloc 更高效
calloc 的好处是自动清零。我写网络程序时,经常用 calloc 初始化缓冲区,省得自己 memset。
realloc:调整已申请内存的大小
int *arr = (int *)malloc(5 * sizeof(int));
// 想扩容到 10 个
int *new_arr = (int *)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
// realloc 失败,原内存还在
free(arr);
return -1;
}
arr = new_arr; // 更新指针
realloc 有个坑:它可能移动内存块。如果后面空间不够,realloc 会重新找一块更大的空间,把旧数据拷贝过去,然后释放旧内存。所以 一定要用返回值更新指针,不能直接用原指针。
我的经验:
realloc 失败时,原内存不会被释放。所以别写成 arr = realloc(arr, new_size),这样如果 realloc 失败,arr 变成 NULL,原内存就泄漏了。正确做法是用临时变量接收返回值。
2.4 常见内存错误:你一定会遇到
做 C 语言开发,内存错误几乎是躲不开的。我列几个最常见的,你看看中过几个:
| 错误类型 | 表现 | 后果 |
|---|---|---|
| 内存泄漏 | malloc 后忘记 free | 程序内存持续增长,最终 OOM |
| 野指针 | free 后继续使用 | 数据错乱、崩溃 |
| 缓冲区溢出 | 写入超过申请的大小 | 破坏相邻内存、安全漏洞 |
| 重复释放 | 对同一内存 free 两次 | double free 崩溃 |
| 使用未初始化内存 | malloc 后直接读取 | 随机值、不可预测行为 |
内存泄漏:这是最常见的问题。我维护过一个老项目,跑三天就挂,一查发现是某个循环里不断 malloc 但不 free。修复后连续跑了三个月没问题。
缓冲区溢出:这个更危险。你申请了 10 个字节,却写了 20 个,多出来的 10 个字节会覆盖后面的内存。攻击者可以利用这个漏洞执行任意代码。所以写代码时一定要检查边界。
使用未初始化内存:malloc 返回的内存内容是随机的。直接读取会得到垃圾值。我建议要么用 calloc,要么 malloc 后立刻 memset。
记住这个原则:
谁申请,谁释放。谁打开,谁关闭。这是 C 语言内存管理的基本纪律。
2.5 本章知识体系
下面这张图把本章的核心内容串起来了,你可以对照着复习:
这张图把堆与栈、malloc/free、calloc/realloc、常见内存错误串在了一起。你仔细看看,每个模块之间都有联系。比如堆内存用 malloc 申请,用 free 释放,如果忘了 free 就造成内存泄漏。再比如 realloc 本质上也是 malloc + 拷贝 + free 的组合操作。
嗯,这一章的内容就这些。内存管理是 C 语言的硬骨头,但也是基本功。你把这些概念吃透了,后面讲内存泄漏检测方法时,才能理解为什么会有泄漏、怎么去定位泄漏。
可可精品资料库