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 本章知识体系

下面这张图把本章的核心内容串起来了,你可以对照着复习:

C语言内存管理基础 堆与栈 栈:自动管理,速度快,空间小 堆:手动管理,速度慢,空间大 栈上变量:函数结束自动释放 堆上变量:需手动 free 释放 注意:栈空间有限,大块数据用堆 malloc/free malloc:申请内存,返回 void* free:释放内存,需置空指针 必须检查 malloc 返回值 避免重复 free 核心:谁申请谁释放 calloc/realloc calloc:申请并初始化为 0 realloc:调整已申请内存大小 realloc 可能移动内存块 必须用临时变量接收 realloc 返回值 常见内存错误 内存泄漏:malloc 后忘记 free 野指针:free 后继续使用 缓冲区溢出:写入超过申请大小 重复释放:double free 核心原则:谁申请谁释放,检查返回值,释放后置空

这张图把堆与栈、malloc/free、calloc/realloc、常见内存错误串在了一起。你仔细看看,每个模块之间都有联系。比如堆内存用 malloc 申请,用 free 释放,如果忘了 free 就造成内存泄漏。再比如 realloc 本质上也是 malloc + 拷贝 + free 的组合操作。

嗯,这一章的内容就这些。内存管理是 C 语言的硬骨头,但也是基本功。你把这些概念吃透了,后面讲内存泄漏检测方法时,才能理解为什么会有泄漏、怎么去定位泄漏。

可可精品资料库