C 语言指针与内存管理
更新时间:2026-08-25。本文是
languages/c/主题第 ② 篇。指针是 C 的灵魂,也是绝大多数崩溃与性能问题的源头——搞懂"地址、生命周期、越界"三个词,就掌握了 C 内存管理的全部要领。
本文要回答的问题
- 指针到底是什么?为什么
a[1]等于*(a+1)? - malloc 的内存从哪来?
free之后指针为什么还在? - 悬垂指针、内存泄漏、越界访问分别是怎么发生的,如何用工具定位?
一、指针本质:地址 + 类型
1.1 指针就是地址,类型决定步长
c
int x = 10;
int *p = &x; // p 的值 = x 的地址
long *q = (long *)&x; // 类型转换:同一地址,不同解释
printf("%p\n", (void *)p); // 0x...(地址)
printf("%ld\n", *q); // 危险!把 4 字节 int 按 8 字节 long 读指针变量存的是地址;指针的类型决定:解引用时读多少字节、指针 +1 时跳多少字节:
| 指针类型 | +1 步长 | 解引用读入 |
|---|---|---|
char * | 1 字节 | 1 字节 |
int * | 4 字节 | 4 字节 |
double * | 8 字节 | 8 字节 |
struct Foo * | sizeof(Foo) 字节 | 整个结构体 |
这正是 L5 ELF 类型系统 与 L3 对齐 的衔接点:
+1的步长由编译器按类型计算,而结构体对齐保证每个成员落在正确的字节边界。
1.2 数组退化为指针
c
int a[4] = {1, 2, 3, 4};
int *p = a; // 数组名退化为首元素地址,等价于 &a[0]
a[2] == *(a + 2); // true
p[2] == *(p + 2); // true,指针也能用下标
sizeof(a) == 16; // 数组用 sizeof 得到总字节数
sizeof(p) == 8; // 指针用 sizeof 只有 8(64 位)区分:sizeof(a) 是整个数组大小,sizeof(p) 只是指针大小——数组名在 sizeof 与 &a 时不退化,其余几乎都退化。
二、进程内存布局

| 区域 | 谁管理 | 生命周期 | 典型内容 |
|---|---|---|---|
| 代码段 text | 编译器 | 进程全程 | 机器指令,只读 |
| 数据段 | 编译器 | 进程全程 | 全局/静态变量(BSS 零初始化) |
| 堆 heap | 程序员 malloc/free | 手动 | 动态分配的数组、对象 |
| 栈 stack | 编译器(调用帧) | 函数调用期间 | 局部变量、参数、返回地址 |
用 cat /proc/<pid>/maps 可看到进程真实的内存布局(每一段地址范围 + 权限位),详见 proc 内存观测。
三、malloc / free 生命周期
3.1 正确用法
c
int *arr = malloc(10 * sizeof(int)); // 分配堆内存
if (arr == NULL) { /* 分配失败处理 */ }
arr[0] = 42; // 使用
free(arr); // 释放
arr = NULL; // 习惯:置空,防悬垂malloc失败返回NULL,使用前必须判空。free只接受malloc/calloc/realloc返回的原始指针,不能 free 栈地址或中间偏移。
3.2 内存泄漏(leak)
c
void leak(void) {
int *p = malloc(100); // 分配后没人 free,函数返回后地址丢失
}- 现象:进程 RSS 不断上涨,
top/ps可观测;长期运行最终 OOM。 - 定位:Valgrind
valgrind --leak-check=full ./prog或 ASan(-fsanitize=address),详见 crash 内存泄漏排查。
3.3 悬垂指针(dangling)
c
int *p = malloc(sizeof(int));
free(p);
*p = 42; // 悬垂!释放后继续使用(use-after-free)- 释放后指针仍保存原地址,再解引用是未定义行为——可能读到垃圾、可能崩溃。
- 与泄漏相反:泄漏是"忘了释放",悬垂是"释放后还用"。两者都是 crash 排查线 的高频主题。
3.4 越界访问(out-of-bounds)
c
int a[4];
a[4] = 5; // 越界写:写了 a 后面 4 字节,可能是别的变量
for (int i = 0; i <= 4; i++) a[i] = i; // <= 是经典的 off-by-one- 越界读/写不一定会立刻崩溃,但会悄悄破坏相邻内存——最难查的 bug 之一。
- 栈越界可用
-fsanitize=address直接抓;堆越界 Valgrind 也能报。
四、常见指针陷阱对照
| 陷阱 | 示例 | 后果 | 工具/预防 |
|---|---|---|---|
| 解引用 NULL | *p 当 p==NULL | 段错误 SIGSEGV | 判空 |
| 悬垂指针 | free 后使用 | 未定义行为/崩溃 | 置 NULL、ASan |
| 内存泄漏 | malloc 不 free | RSS 上涨/OOM | Valgrind、ASan |
| 越界写 | a[4]=5 | 破坏相邻内存 | ASan、Valgrind |
| 双重释放 | free 两次 | 堆损坏 | free 后置 NULL |
| 类型不匹配 | long* 读 int | 读错字节 | 保持类型一致 |
| 返回值是局部地址 | return &local | 悬垂 | 用堆或 static |
五、与本站主线衔接
- 崩溃线:段错误、ASan/Valgrind 完整打法见 crash 排查线。
- 性能线:堆分配频繁会成为分配热点,
perf可剖析到malloc调用栈;内存访问的缓存/TLB 影响见 L3 内存子系统。 - 观测:
pmap、/proc/<pid>/maps看实际布局,见 内存观测工具。 - ELF 视角:数据段/BSS/堆栈在可执行文件中的对应关系,见 编译、链接与 ELF。
一句话总结
C 指针与内存管理 = 记住三件事:指针是带类型的地址、malloc 分配堆内存要配 free、越界/悬垂/泄漏三类错误各有工具可查(ASan/Valgrind);掌握它,C 程序 90% 的崩溃与性能问题都有了解法。
上一节:C 语言主题总纲 下一节:编译、链接与 ELF