Appearance
进程内存布局:虚拟内存 / 堆 / 栈
一、进程虚拟地址空间(64 位 Linux x86-64)
0x7FFF FFFF FFFF ┌──────────────────┐ 高地址(往下长)
│ 栈 Stack │ 向下生长(高→低),含局部变量/函数帧
│ ↓ │
│ (空洞/共享区) │ 共享库 mmap 段
│ ↑ │
│ 堆 Heap │ 向上生长(低→高),malloc/new
│ .bss (未初始化全局/static)│
│ .data(已初始化全局/static)│
│ .rodata(只读常量) │
│ .text(代码段) │
0x0000 0000 0000 └──────────────────┘ 低地址关键概念:
- 这只是虚拟地址空间,页表映射到物理页;未访问的页不实际占物理内存(按需调页)。
.text(代码)、.data(已初始化全局/静态)、.bss(未初始化全局/静态,不占文件只占内存)、.rodata(字面量常量)、堆、栈、共享库 mmap。
二、C++ 的内存分区(教材口径)
- 代码区(text):函数代码,只读。
- 全局/静态存储区(data+bss):全局变量、静态变量——程序启动分配、结束释放。
- 常量区(rodata / 也可能是 text 后段),只读:字符串字面量等。
- 栈区:局部变量、函数参数、返回地址——编译期已定大小,随调用分配释放,速度快,有大小上限(Linux 默认栈 8MB
ulimit -s)。 - 堆区:程序员管理(new/malloc),由低向高增长,慢、易碎片。
常混淆:只读常量直接量一般归于常量区(可在 data/rodata);很多人把它并入「全局区」。面试可答「广义上全局常量在 data 只读段」。重点讲清楚 栈 vs 堆 的特征。
三、栈 vs 堆 对比
| 维度 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 分配 | 编译器自动(函数帧) | 程序员 new/malloc 手动 |
| 方向 | 高地址向低地址(向下) | 低地址向高地址(向上) |
| 速度 | 极快(移动栈指针) | 慢(找空闲块/系统调用) |
| 生命周期 | 函数作用域结束即释放 | 需手动释放或用 RAII |
| 大小限制 | 有(Linux 8MB,可 ulimit) | 可到虚拟内存上限(通常很大) |
| 碎片 | 无(后进先出规整) | 有(malloc 分配/释放不规整) |
| 多线程 | 每线程独立栈 | 进程内所有线程共享堆(需同步) |
| 越界风险 | 栈溢出(递归过深) | 泄漏 / use-after-free / 野指针 |
| 谁能返回 | 栈上数组不能把局部地址当返回值给上层长期使用(悬垂) | 可(用智能指针管理) |
记忆点:
- 栈 = 快 + 自动 + 有上限 + 连续规整;堆 = 灵活 + 大 + 需管理。
- 线程独立栈:递归深度过大 → 栈溢出(crash):经典题「深递归爆栈/段错误」。
- 栈对象:通常小而生命周期短、或 RAII 包裹堆资源。
四、函数调用压栈
调用 f(a,b):
- 参数按调用约定入栈(多数 x86-64 ABI:前 6 整数参数用寄存器,多的才入栈)。
- 压入返回地址。
call跳到 f:保存栈帧指针(rbp),分配局部变量空间(rsp 调整)。- 返回时恢复,栈指针回弹(栈即「后进先出」,释放即指针回弹,天然无碎片)。
五、高频题
Q1. 栈空间 vs 虚拟内存大小;为什么递归太深会栈溢出?
每个线程栈默认约 8MB,递归每层消耗帧 → 深度过大爆栈(Segmentation fault)。规避:尾递归/改循环/加大线程栈(pthread_attr_setstacksize)/把大结构放堆。
Q2. 堆为什么比栈慢?
分配要经过用户态 free-list 查找 + 可能系统调用 brk/mmap;且有锁、缓存不友好 (heap 对象分散导致 cache miss 高于连续栈帧)。栈通常就在当前 cache line、只改指针。
Q3. 全局对象构造/析构顺序?
构造:定义序(跨翻译单元顺序未定——知名坑「Static Initialization Order Fiasco」,可用局部 static / Meyers Singleton 规避)。析构:构造逆序。
Q4. malloc 返回的地址是物理地址吗?
不是,是虚拟地址;由 OS+页表映射,程序员不感知物理地址。