C程序的内存到底怎么分的?栈、堆、全局区一张图讲清楚
你有没有想过一个问题:你写的每一个变量,到底存在内存的哪个位置?
int a = 10; 存在哪?malloc 分配的内存存在哪?static int count; 又存在哪?
很多人写了好几年 C 代码,对这些还是一笔糊涂账。结果就是:栈溢出了不知道为什么,内存泄漏了找不到在哪,全局变量改了那个文件出了 bug 更想不通。
今天把 C 程序的内存布局彻底拆清楚。看完这篇,以后遇到内存问题,你至少知道该往哪个方向查。
C程序内存的5大区
一个 C 程序运行时,内存被分成 5 个区域。从低地址到高地址依次是:
arduino
┌─────────────────┐ 高地址
│ 栈区 Stack │ ↓ 向下增长
│ ... │
│ 空闲 │
│ ... │
│ 堆区 Heap │ ↑ 向上增长
│ .bss(未初始化) │
│ .data(已初始化)│
│ 代码段 .text │
└─────────────────┘ 低地址
每张区域存什么?一句话总结:
| 区域 | 存什么 | 谁管理 | 生命周期 |
|---|---|---|---|
| 代码段 | 机器指令 | 系统 | 程序运行期间 |
| 全局区(.data/.bss) | 全局变量、static变量、字符串常量 | 系统 | 程序运行期间 |
| 堆区 | malloc/calloc 分配的内存 | 程序员手动 | 手动 free 前 |
| 栈区 | 局部变量、函数参数 | 系统自动 | 函数调用期间 |
| (栈帧之间) | 函数返回地址、寄存器保存 | 系统自动 | 函数调用期间 |
💡 最关键的区别 :栈和全局区是系统自动管理,堆是你自己管理。C 语言不帮你擦屁股,忘了 free 就是泄漏。
栈区------最快也最小的内存
栈的特点:自动分配、自动回收、速度极快、但空间很小。
c
void func(int a) {
int b = 10; // 栈
char buf[1024]; // 栈
double arr[100]; // 栈
} // 函数结束,a、b、buf、arr 全部自动释放
栈的空间有多大?
- Linux :默认 8MB(
ulimit -s查看) - Windows:默认 1MB
听起来不大,但对大多数函数来说足够了。问题出在两种情况:
情况一:局部数组太大
c
// ❌ 危险:大数组直接撑爆栈
void bad_function() {
int arr[1000000]; // 4MB,在 Windows 上直接栈溢出
}
正确做法:大数组放堆上
c
// ✅ 大内存分配用 malloc
void good_function() {
int *arr = (int *)malloc(1000000 * sizeof(int));
if (arr == NULL) return;
// 使用 arr...
free(arr);
}
情况二:递归太深
c
// ❌ 无限递归 → 栈溢出
void infinite() {
int temp[100]; // 每次调用都占 400 字节栈空间
infinite(); // 永远不返回,栈指针一直下移
}
💡 记住:栈空间是固定的,不会动态扩展。局部变量小、函数调用浅 → 栈够用;大数组、深递归 → 换堆。
堆区------你自己管的大仓库
堆的特点:手动分配、手动释放、空间很大、但有碎片风险。
c
// 分配
int *p = (int *)malloc(100 * sizeof(int)); // 400 字节
if (p == NULL) {
perror("分配失败");
return;
}
// 使用
for (int i = 0; i < 100; i++) {
p[i] = i;
}
// 释放------忘了就是泄漏
free(p);
p = NULL; // 好习惯:释放后置空
malloc vs calloc vs realloc
c
// malloc:分配,内容不初始化(随机值)
int *a = (int *)malloc(10 * sizeof(int));
// calloc:分配 + 清零
int *b = (int *)calloc(10, sizeof(int)); // 10个int,全部为0
// realloc:调整大小(可能搬移数据)
int *c = (int *)realloc(b, 20 * sizeof(int)); // 扩到20个int
// ⚠️ realloc 可能失败,失败时原指针 b 仍有效
// 安全写法:
int *temp = (int *)realloc(b, 20 * sizeof(int));
if (temp != NULL) {
b = temp; // 成功后才更新指针
} else {
// 失败处理,b 还能用
}
💡 堆的铁律:
- 每次
malloc必须检查NULL - 每次
malloc必须对应一次free free之后立刻置NULL- 不要
free两次(double free = 崩溃)
全局区------static 和全局变量的家
全局区分为 .data(已初始化)和 .bss(未初始化),程序运行期间一直存在。
c
int g1 = 100; // .data 段(已初始化)
int g2; // .bss 段(未初始化,系统自动清零为0)
static int g3 = 200; // .data 段,但只在本文件可见
const char *str = "hello"; // str 在 .data,"hello" 在只读数据段
void counter() {
static int count = 0; // .data 段,只初始化一次!
count++;
printf("第 %d 次调用\n", count);
}
static 局部变量的"记忆"效果
c
void test() {
int a = 0; // 每次调用都重新初始化为 0
static int b = 0; // 只初始化一次,之后保留上次的值
a++;
b++;
printf("a=%d, b=%d\n", a, b);
}
int main() {
test(); // a=1, b=1
test(); // a=1, b=2 ← b 记住了!
test(); // a=1, b=3
}
💡 static 的本质:把变量从栈区搬到全局区。函数结束了,全局区的变量不会消失,所以能"记住"上次的值。
代码段------你写的代码存在这
代码段存放编译后的机器指令,只读,多个进程可以共享同一份代码。
c
// 以下所有代码,编译后都存在代码段
int add(int a, int b) {
return a + b;
}
int main() {
for (int i = 0; i < 10; i++) {
printf("%d\n", add(i, 1));
}
return 0;
}
💡 代码段是只读的 。所以你不能修改常量字符串:
c
char *str = "hello";
str[0] = 'H'; // 段错误!"hello" 存在只读数据段
但数组可以:
c
char str[] = "hello"; // "hello" 被拷贝到栈上
str[0] = 'H'; // 没问题,栈上的副本可以改
实战:一个程序看清所有区域
c
#include <stdio.h>
#include <stdlib.h>
int global_var = 100; // 全局区 .data
int global_uninit; // 全局区 .bss(自动为0)
static int static_global = 50; // 全局区 .data(本文件可见)
const char *g_str = "hello"; // g_str 在 .data,"hello" 在只读段
void demo(int param) { // param 在栈区
int local = 42; // 栈区
static int static_local = 0; // 全局区 .data(只初始化一次)
int *heap_var = (int *)malloc(sizeof(int)); // 堆区
*heap_var = 999;
printf("--- 内存分布 ---\n");
printf("代码段(函数地址): %p\n", demo);
printf("全局区 .data: %p (global_var=%d)\n", &global_var, global_var);
printf("全局区 .bss: %p (global_uninit=%d)\n", &global_uninit, global_uninit);
printf("只读数据段: %p (str=%s)\n", g_str, g_str);
printf("堆区: %p (*heap=%d)\n", heap_var, *heap_var);
printf("栈区: %p (local=%d)\n", &local, local);
printf("栈区(参数): %p (param=%d)\n", ¶m, param);
free(heap_var);
}
int main() {
demo(77);
return 0;
}
运行后你会看到:
- 代码段地址最低(函数本身的位置)
- 全局区在中间
- 堆区在更高地址
- 栈区在最高地址
💡 这个程序跑一遍,5 个区全看到了。 比看十遍教科书管用。
一张表记住所有区别
| 特性 | 栈 | 堆 | 全局区 | 代码段 |
|---|---|---|---|---|
| 存什么 | 局部变量、参数 | malloc 分配的数据 | 全局/static/常量 | 机器指令 |
| 谁管理 | 系统自动 | 程序员手动 | 系统自动 | 系统 |
| 速度 | 极快 | 较慢 | 快 | --- |
| 大小 | 1~8MB | 几乎不限(受内存制约) | 编译时确定 | 编译时确定 |
| 增长方向 | 高→低 | 低→高 | 固定 | 固定 |
| 生命周期 | 函数调用期间 | 手动 free 前 | 程序运行期间 | 程序运行期间 |
| 常见坑 | 栈溢出、返回局部地址 | 内存泄漏、double free | 全局变量命名冲突 | 修改常量字符串 |
写在最后
C 语言的内存就 5 个区,每个区都有自己的规则:
- 栈:快但小,函数结束自动回收,别放太大的东西
- 堆:大但麻烦,malloc 和 free 必须成对出现
- 全局区:static 变量的家,程序结束才释放
- 代码段:只读的,别碰常量字符串
- 只读数据段:字符串常量的归宿,修改就崩
选择原则很简单:
- 小且短命 → 栈
- 大或变长或跨函数 → 堆
- 需要全程存在 → 全局区/static
理解了内存布局,很多"莫名其妙"的 bug 就有了方向:段错误 → 看是不是碰了只读区;数据莫名被改 → 看是不是栈溢出覆盖了;内存越来越多 → 看是不是 malloc 没 free。
C 语言不会帮你管内存,但你可以帮自己搞清楚。