“堆"和"栈"在 C/C++ 中有两个不同层面的含义:数据结构层面和系统内存管理层面。初学者经常混淆,本文从两个角度分别说清楚。
一、数据结构层面
| 栈(Stack) | 堆(Heap) | |
|---|---|---|
| 规则 | 后进先出(LIFO) | 满足堆性质的完全二叉树(最大堆/最小堆) |
| 用途 | 函数调用栈、表达式求值、DFS | 堆排序、优先队列 |
| 实现 | 顺序栈或链式栈 | 数组(通常) |
这是数据结构课程中的概念,与内存管理无关。
二、系统内存管理层面
这是 C/C++ 编程中更常见的语境。
程序的内存布局
一个 C/C++ 程序的内存分为以下区域:
高地址
┌─────────────────┐
│ 栈区(stack) │ ← 向低地址增长
│ ↓ │
│ │
│ (空闲空间) │
│ │
│ ↑ │
│ 堆区(heap) │ ← 向高地址增长
├─────────────────┤
│ 全局/静态区 │ 全局变量、static 变量
├─────────────────┤
│ 常量区 │ 字符串常量、const 常量
├─────────────────┤
│ 代码区 │ 函数体的机器指令
└─────────────────┘
低地址
栈(Stack)
由系统自动管理,用于存放函数参数、局部变量、返回地址等。
特点:
- 分配/释放速度极快(只需移动栈指针)
- 大小有限(Windows 下默认 1MB,Linux 下默认 8MB)
- 内存连续
- 函数结束时自动释放
void foo() {
int a = 10; // 栈上分配
char buf[256]; // 栈上分配
// 函数返回时,a 和 buf 自动释放
}
堆(Heap)
由程序员手动管理(malloc/free、new/delete),用于动态分配内存。
特点:
- 大小灵活,受限于系统虚拟内存
- 内存不连续(通过链表管理空闲块)
- 分配/释放较慢(需要搜索空闲链表)
- 忘记释放会导致内存泄漏
void bar() {
int *p = new int[1000]; // 堆上分配
// ...
delete[] p; // 必须手动释放
}
栈 vs 堆 对比
| 维度 | 栈 | 堆 |
|---|---|---|
| 管理 | 系统自动 | 程序员手动 |
| 速度 | 快(指针移动) | 慢(链表查找) |
| 大小 | 小(MB 级) | 大(受虚拟内存限制) |
| 连续性 | 连续 | 不连续 |
| 生命周期 | 函数结束自动释放 | 需手动释放 |
| 碎片 | 无 | 容易产生 |
三、实例:各变量存放在哪里
int g = 0; // 全局区(已初始化)
char *p1; // 全局区(未初始化)
int main() {
int b; // 栈
char s[] = "abc"; // 栈(数组在栈上,内容运行时复制)
char *p2; // 栈(指针本身在栈上)
char *p3 = "123456"; // 栈(指针),"123456" 在常量区
static int c = 0; // 全局/静态区
p1 = (char *)malloc(10); // malloc 分配的 10 字节在堆上
p2 = (char *)malloc(20); // malloc 分配的 20 字节在堆上
strcpy(p1, "123456"); // "123456" 在常量区,复制到堆上
// 编译器可能将 p1 和 p3 指向的 "123456" 优化为同一份
}
四、存取效率对比
char s1[] = "hello"; // 数组在栈上,内容运行时复制
char *s2 = "world"; // 指针在栈上,指向常量区的字符串
访问 s1[0]:通过栈帧偏移直接读取,一条指令。
访问 s2[0]:先从栈上读取指针值,再通过指针间接访问目标地址,两条指令。
; s1[1] — 直接通过栈帧偏移读取
mov cl, byte ptr [ebp-0Fh]
; s2[1] — 先读指针,再间接寻址
mov edx, dword ptr [ebp-14h] ; 读取指针
mov al, byte ptr [edx+1] ; 间接访问
栈上数组的访问效率略高于指针间接访问,因为少了一次内存读取。
五、总结
比喻:
- 栈 像去餐馆吃饭——点菜、吃、走人,快但选择有限
- 堆 像自己做饭——买菜、做菜、洗碗,慢但自由度高
一句话: 栈是系统管、快而小;堆是自己管、慢而大。
注意:操作系统层面的"堆"和数据结构中的"堆"是完全不同的概念,只是英文都叫 heap。日常编程中说"堆和栈”,通常指内存管理层面。