C 语言栈与堆
更新时间:2026-08-26。本文是
languages/c/主题入门层第 50 篇。写 C 的都知道变量要"放内存里",但放哪块、谁能活多久,是入门到进阶的分水岭。栈自动管理、速度快但有大小限制;堆随意分配、生命周期你说了算但要亲手回收。这一篇把两者的账算清楚。
本文要回答的问题
- 栈和堆分别是什么?变量默认放哪?
- 为什么局部变量一离开函数就"消失"?
- 栈溢出是什么?堆泄漏又是什么?
- 什么时候该用堆?
一、两个区域,两种命运
一个 C 进程的内存布局大致如下(地址从高到低):
高地址 ┌──────────────┐
│ 栈 │ ← 向下增长,局部变量
│ ↓↓ │
│ (空 闲) │
│ ↑↑ │
│ 堆 │ ← 向上增长,malloc 分配
│ 全局/静态区 │ ← 全局变量、static 变量
│ 只读常量区 │ ← 字符串字面量
低地址 └──────────────┘栈(stack):函数调用时自动分配一块"栈帧",放局部变量;函数返回时自动释放。先进后出(LIFO),像一摞盘子。
堆(heap):程序主动用 malloc 申请的内存,free 之前一直活着,跟函数调用无关。像仓库,租了就占着,退租才腾出来。
二、栈:自动管理,但空间有限
c
void foo(void) {
int x = 10; // 分配在 foo 的栈帧里
char buf[1024]; // 大数组也在栈上
} // foo 返回,x、buf 自动消失关键特性:
| 特性 | 说明 |
|---|---|
| 分配速度 | 极快,就是移动栈指针 |
| 管理方式 | 自动:进入函数分配、返回释放 |
| 空间大小 | 有限(默认约 8MB),受 ulimit -s 控制 |
| 生命周期 | 到所属函数返回为止 |
| 越界后果 | 栈溢出(stack overflow),程序崩溃 |
为什么局部变量一返回就"没"了?因为它的内存属于那次函数调用的栈帧,返回后栈指针恢复,内存被标记为"可复用"——下一个调用直接覆盖。所以返回局部数组/指针是未定义行为。
三、堆:手动管理,随你掌控
c
#include <stdlib.h>
int *p = malloc(100 * sizeof(int)); // 在堆上要 100 个 int
if (p == NULL) return 1; // 分配失败要处理
// ... 用 p ...
free(p); // 用完必须释放!| 特性 | 说明 |
|---|---|
| 分配速度 | 相对慢(要查空闲块、可能走系统调用) |
| 管理方式 | 手动:malloc/free 自己管 |
| 空间大小 | 很大(取决于系统内存/地址空间) |
| 生命周期 | 从 malloc 到 free,可跨函数存活 |
| 忘记 free | 内存泄漏 |
典型场景:数量运行时才知道的数据(比如用户输入决定数组大小)、需要跨多个函数共享的数据、特别大的缓冲区。
四、栈溢出 vs 内存泄漏:两个反面教材
栈溢出——无限递归或超大局部数组:
c
void recurse(void) {
char big[1024 * 1024]; // 1MB,一层就不少
recurse(); // 无限递归,栈被撑爆
}症状:Segmentation fault / stack overflow。递归要有终止条件(递归那篇强调过)。
内存泄漏——malloc 了不 free:
c
void leak(void) {
int *p = malloc(1024);
// 忘了 free(p)!
}循环调用 leak 一万次,堆上积累 10MB 永远拿不回来,程序越跑越胖。长期运行的服务(服务器、守护进程)泄漏是致命伤。
五、怎么选:栈还是堆?
| 判断 | 用栈 | 用堆 |
|---|---|---|
| 大小已知且不大 | ✅ | |
| 函数返回后还要用 | ✅ | |
| 多个函数共享 | ✅ | |
| 数量运行时才确定 | ✅ | |
| 追求速度 | ✅ |
经验法则:能用栈就用栈(快、免管理);数据量不确定或生命周期要跨函数,用堆并保证成对释放。
六、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 返回局部变量地址 | 悬垂指针,值随机 | 堆分配或调用方传缓冲区 |
| 超大局部数组 | 栈溢出 | 用 malloc 或 static |
| 无限递归 | stack overflow | 检查终止条件 |
| malloc 忘 free | 内存泄漏 | 谁分配谁释放 + 工具检测 |
| 栈上数组转给别处用 | 悬垂 | 明确所有权 |
七、与本站主线衔接
- 堆分配的具体 API 与规则,见malloc 与 free;
- 栈与堆在系统层面的实现(内核栈、用户栈、mmap 的堆),见进程地址空间与内存布局;
- 栈溢出与段错误的底层机制,见段错误与 core dump;
- 检测泄漏的工具,见Valgrind 内存检测。
一句话总结
栈由函数调用自动管理、快但有限(默认 ~8MB),局部变量默认放栈上、返回即失效;堆用 malloc/free 手动管理、大且生命周期可控但必须亲手释放——能栈则栈,要跨函数/不确定大小就上堆,别忘配对的 free。