C 语言 - 堆和栈解析
预先了解
栈指针(Stack Pointer, SP)
-
栈指针又称栈顶指针。就像 栈的“手指”,指向 栈顶 的位置。它告诉 CPU / 系统:“栈顶在哪里,下一个要放的东西应该放在哪儿。”
-
假设你的栈是一个竖着的书堆:
高地址 ↑ [空闲内存] [局部变量2] ← 先入栈的变量(稍早定义) [局部变量1] ← 后入栈的变量(稍晚定义) [新的局部变量] ← 栈顶,SP 指向这里 低地址 ↓
-
-
栈指针的工作方式:
- 入栈(push) → 把新变量放到栈顶,栈指针向下移动(在大多数系统中栈向低地址增长)
- 放一本书 → 手指往下数,指向新放的书 → SP 更新到新栈顶
- 出栈(pop) → 栈指针向上移动,变量被“弹出”(释放)
- 拿一本书 → 手指往上数,指向上一本书 → SP 回退
- 入栈(push) → 把新变量放到栈顶,栈指针向下移动(在大多数系统中栈向低地址增长)
-
栈指针就像这个手指,告诉你栈顶的位置
-
C 语言中的体现:
- 定义局部变量,系统通过 栈指针(SP) 来自动分配空间
- 当函数结束,SP 回退,变量自动释放
- 不用直接操作 SP(除非写汇编或底层嵌入式程序)
malloc 分配 — “向堆要空间”
-
全称
memory allocation(内存分配):在 堆(Heap) 上申请一块可以存储数据的空间。堆就像一个大书架,malloc就是去书架上借一个空格放书。告诉系统:“我要一块大小 X 的空间”,系统找到合适的空闲区域,给你一个地址 -
举例:
int *p; p = (int *)malloc(sizeof(int)); // 申请一个int大小的空间(32位系统中可以理解为4字节) *p = 42; // 往里面放数据 //p 保存的是这块空间的地址 //你可以通过 *p 来读写这个空间
free 释放 — “把空间还回去”
-
把之前
malloc分配的空间 释放 回堆,让系统可以重新使用- 这是必须的,如果不
free→ 空间一直占用 → 内存泄漏(Memory Leak)。就像你借了书架的空格放书,用完要把书拿走,把空格归还给书架,否则别人就没地方放书了。
- 这是必须的,如果不
-
举例:
free(p); // 释放 p 指向的空间 p = NULL; // 好习惯:避免悬空指针
栈(Stack)——“自动整理的书堆”
-
比如在书桌上整理书:
- 每次用书就往书堆顶部放,用完就从顶部拿走,越早放的也就堆到越后面才能使用,也就是 先进后出(LIFO) 的原则。
- 先进后出(LIFO):先放进去的,后拿出来。栈顶就是最新放入的位置。
- 理解了栈指针后不难看出:栈的出栈和栈指针的出栈是相反的
- 每次用书就往书堆顶部放,用完就从顶部拿走,越早放的也就堆到越后面才能使用,也就是 先进后出(LIFO) 的原则。
-
栈的全过程运作举例:
#include <stdio.h> void func(int a, int b) { int sum = a + b; // 在 func 的栈帧中为 sum 分配空间(SP -= 4),让 SP 所指的地址往低 4 字节移动 // int 占 4 个字节(这是标准 32 位系统的常见情况) //当编译器看到 int sum; 时,会生成汇编大致如下: //sub sp, sp, #4 ; SP = SP - 4,为 sum 分配 4 字节空间 //总占用空间:sum、a、b等3个变量+返回地址(4*4).因此占用16字节 printf("sum = %d\n", sum); // 调用 printf,会再建立一个新的栈帧 } // 函数结束:SP += func 栈帧大小(sum、a、b、返回地址(func = 4*4) 全部出栈) //因此 SP = SP + (4*4) 归0找回空间 int main(void) { int x = 2; // main 栈帧中分配 x (SP -= 4) int y = 3; // main 栈帧中分配 y (SP -= 4) func(x, y); // 调用 func 时执行如下操作(由 CPU + 编译器自动完成): // ① 把参数 b 压栈(SP -= 4) // ② 把参数 a 压栈(SP -= 4) // ③ 把返回地址压栈(SP -= 4) // ④ 跳转到 func() // // func() 运行完毕后: // ⑤ 弹出返回地址(SP += 4) // ⑥ 清理参数栈空间(SP += 8) return 0; // main 结束,整个程序栈由系统回收 } // 一个程序只有一个返回地址 //返回地址:当 CPU 执行类似 call func 的指令时,会把下一条指令的地址(即调用指令之后的位置)压入栈中,这就是返回地址 //目的:函数执行完后能回到调用点继续执行 //栈上只存这个返回地址,大小在 32 位系统上通常是 4 字节 -
特点:
- 自动管理 → 函数里定义的局部变量,函数调用时自动分配,函数结束自动释
- 快速 → 系统直接调整栈指针即可放
- 空间有限 → 栈太大容易溢出
-
总结:栈就像桌上的书堆,每次放在顶上,用完从顶上拿走,整齐又快
堆(Heap)——“书架上的自由摆放”
-
比如有一个大书架,可以随意把书放在书架上任何位置,需要的时候自己去找。堆就是 动态分配,系统不会自动管理。
-
特点:
- 手动管理 → 需要程序员通过
malloc分配,free释放 - 灵活 → 空间大,可以存放各种大小的数据
- 慢一点 → 系统要找到合适的空闲空间
- 手动管理 → 需要程序员通过
-
总结:堆就像书架,想放哪就放哪,但要记得收好,否则书架会乱掉
栈和堆的区别
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 存储内容 | 局部变量、函数参数、返回地址 | 动态分配的对象或数据 |
| 分配方式 | 编译时确定 / 函数调用时自动分配 | 程序运行时手动分配(malloc / new) |
| 生命周期 | 函数调用开始到结束 | 程序员控制,手动释放(free / delete) |
| 管理方式 | CPU + 编译器自动管理 | 程序员或垃圾回收管理 |
| 分配速度 | 快(连续空间,LIFO) | 相对慢(需要查找空闲块) |
| 大小限制 | 较小(通常几 MB) | 较大(取决于可用内存) |
| 访问方式 | 地址连续,可通过偏移访问 | 地址不连续,通过指针访问 |
| 典型用途 | 临时变量、函数调用 | 大型对象、动态数组、链表、树等结构 |
-
简单举例:
#include <stdio.h> #include <stdlib.h> void func() { int a = 10; // 栈上分配,函数结束后自动回收 // *p 指针本身 → 栈上分配 int *p = (int *)malloc(sizeof(int)); // 内容在堆上分配 *p = 20; /* 栈: | p (指针) | 4 字节,存储堆地址 | a (局部变量) | 4 字节 ----------------- 堆: | *p (int) | 4 字节,存储实际值 */ printf("a = %d, *p = %d\n", a, *p); free(p); // 手动释放堆内存 } int main() { func(); return 0; }
结论
- 栈:自动、快速、空间小,就像桌上的书堆。
- 堆:手动、灵活、空间大,就像书架上的书,需要自己管理。

浙公网安备 33010602011771号