本节摘要:程序运行时,操作系统把可执行文件的各个"节"映射进一块虚拟地址空间,配合内核分配的栈与运行时库管理的堆,构成进程内存全景。本节画出这张地图并逐区讲解,再给出"任意一个变量落在哪"的判定方法——这是后续指针、动态内存、递归各章的公共地基。

从下往上是:低地址页(封死,防止空指针乱闯)、代码段、数据段、BSS、堆(向上长)、共享库区、栈(向下长)。堆与栈相向而行,中间是待分配的空档——64 位地址空间巨大,实际几乎不会撞上,但栈本身的尺寸上限是有限的。
用一段程序把五类变量各就各位:
#include <stdio.h> #include <stdlib.h> int g_init = 42; /* data 段 */ int g_zero; /* bss 段,加载时为 0 */ static int g_hidden; /* bss 段,私有符号 */ int main(void) { int local = 7; /* 栈 */ static int s_local = 3; /* data 段,不随函数返回销毁 */ int *p = malloc(100 * sizeof(int)); /* p 在栈上,100 个 int 在堆上 */ char *msg = "hello"; /* p 在栈上,字符串本体在只读区 */ printf("g_init=%p g_zero=%p\n", (void*)&g_init, (void*)&g_zero); printf("local=%p s_local=%p\n", (void*)&local, (void*)&s_local); printf("heap=%p msg=%p main=%p\n", (void*)p, (void*)msg, (void*)main); free(p); return local * 0 + g_init * 0 + g_hidden * 0 + s_local * 0; }
一次典型运行输出(地址每次启动不同,但相对关系稳定):
g_init=0x404030 g_zero=0x404038 local=0x7ffd3a2b1c44 s_local=0x404034 heap=0x1a2b3c4d010 msg=0x402004 main=0x401136
读地址就能看出地图:0x4040xx 一片是 data/bss;0x7ffd... 是栈(靠近栈区高地址);0x1a2b... 是堆;0x4011xx 是代码段里的 main。地址是分片聚居的——这是判定"变量住在哪"最直接的经验法则。
另外两个要点藏在这段代码里:
static 修饰局部变量改变的是位置与生命周期(搬到 data/bss,进程期间常驻),不是可见性(函数外依旧不可见)。msg[0]='H' 会直接段错误。它不是"存在指针里的字符串",指针只是一个 8 字节的地址盒子。上面所有地址都是虚拟地址。每个进程看到的是自己一整套地址空间,由 CPU 的内存管理单元(MMU)按页(通常 4KB)翻译成物理地址。好处直接体现在 C 程序员的生活里:
malloc 一大块内存只登记了地址范围,真正写入那页时才分配物理页——所以"malloc 成功"不代表物理内存已经到手;段错误(segfault)的本质也就清楚了:访问的虚拟地址在当前进程的映射表里没有合法条目,MMU 报页错误,内核转成 SIGSEGV 信号,默认行为是终止进程。"段错误"不是 C 语言的概念,是操作系统与硬件协作的产物。
| 你写下的东西 | 落在哪 | 谁分配 | 何时失效 |
|---|---|---|---|
| 局部变量(含参数副本) | 栈 | 编译器生成的代码 | 函数返回 |
| 局部 static 变量 | data 或 bss | 加载器 | 进程结束 |
| 全局变量 / 全局 static | data 或 bss | 加载器 | 进程结束 |
| malloc 家族返回的内存 | 堆 | 运行时分配器 | free 时 |
| 字符串字面量 | 只读区 text | 加载器 | 进程结束 |
| 函数与代码 | text | 加载器 | 进程结束 |
| 局部数组 | 栈(整块连续) | 编译器生成的代码 | 函数返回 |
⚠️ 常见坑:返回局部变量的地址。函数返回后栈帧作废,那块内存随时被下一次调用覆写——拿到地址的调用方读到的可能是任何东西,运气差时还能"碰巧正确",毒害更深。返回堆内存或 static 缓冲区才安全,前者要记得 free。
💡 关键直觉:看到变量先问三个问题——住在哪个区、谁负责分配、什么时候死。三个都答得出,指针类 bug 的数量会断崖式下降。
至此第 1 章完结。下一章我们把镜头从"程序整体"缩到"一个数据":变量在字节层面长什么样,整数为什么用补码,浮点数的 8 个字节里藏着什么。