内存模型¶
C++ 程序运行时的内存(虚拟地址空间)通常分为以下几个区域,从 低地址 到 高地址 大致排列如下:
高地址
┌──────────────────┐
│ 栈(Stack) │ ← 向下增长:局部变量、函数调用栈帧
├──────────────────┤
│ ↓ │
│ (空闲区) │
│ ↑ │
├──────────────────┤
│ 堆(Heap) │ ← 向上增长:new / malloc 动态分配
├──────────────────┤
│ BSS 段 │ 未初始化 / 零初始化的全局变量、静态变量
├──────────────────┤
│ 数据段(.data) │ 已初始化的全局变量、静态变量
├──────────────────┤
│ 只读段(.rodata)│ 字符串字面量、const 常量
├──────────────────┤
│ 代码段(.text) │ 机器指令(只读)
└──────────────────┘
低地址
| 分区 | 存放内容 | 特点 |
|---|---|---|
代码段 .text |
编译后的机器指令 | 只读、可共享 |
只读段 .rodata |
字符串字面量、const 全局变量 |
只读,修改会崩溃 |
数据段 .data |
已初始化 的全局变量、static 变量 |
可读写,程序结束时释放 |
BSS 段 .bss |
未初始化 的全局变量、static 变量 |
自动清零,不占可执行文件空间 |
| 堆 Heap | new / malloc 动态分配的内存 |
向上增长,需手动释放 |
| 栈 Stack | 局部变量、函数参数、返回地址 | 向下增长,自动管理 |
要点:
1 栈¶
栈用于管理 函数调用,每个函数调用会创建一个 栈帧(Stack Frame):
栈帧内容:
┌────────────────┐
│ 返回地址 │
├────────────────┤
│ 函数参数 │
├────────────────┤
│ 局部变量 │
├────────────────┤
│ 保存的寄存器 │
└────────────────┘
特点:
- 自动管理:进入函数自动分配,返回自动释放,无需手动干预
- 速度快:分配/释放只是移动栈指针(一条指令)
- 空间有限:通常几 MB(如 Linux 默认 8MB),递归过深会 栈溢出(Stack Overflow)
- 向下增长:地址从高到低
栈溢出的原因
- 递归过深
- 局部变量过大
- 调用链过长
- 递归深度 × 单层栈帧大:是 1 和 2 的组合,每层递归的局部变量都很大,很快爆栈
2 堆¶
堆用于 运行期动态分配 的内存:
特点:
- 手动管理:
new/delete配对,忘记delete会 内存泄漏 - 空间大:受限于系统可用内存(远大于栈)
- 速度慢:分配需要查找空闲块、系统调用等
- 向上增长:地址从低到高
- 生命周期灵活:不随函数返回销毁,跨函数/跨线程存活
堆和栈的区别
- 管理方式:栈自动管理;堆手动管理
- 空间大小:栈很小(MB 级别);堆很大(GB 级别,受限于虚拟内存)
- 生长方向:栈向低地址扩展(向下生长);堆向高地址扩展(向上生长)
- 分配效率:栈在底层直接移动寄存器指针,速度极快;堆需要调用系统 API(如 brk 或 mmap)并搜索空闲内存块,速度慢
- 连续性与碎片:栈空间是完全连续的;堆频繁 new/delete 会导致大量的内存碎片,降低内存利用率
new/delete 与 malloc/free 的区别
- 属性不同:
new/delete是 C++ 的关键字(操作符),受编译器支持且可以被重载;malloc/free是 C 语言的库函数,需要引入头文件<cstdlib> - 构造与析构:
new在分配内存后会自动调用该类的构造函数,delete会先调用对象的析构函数再释放内存。而malloc/free只是冷冰冰地划出一块原始内存,完全不管对象的初始化和销毁 - 返回类型安全:
new操作成功后返回的是对象类型的指针,类型安全;malloc成功后返回void*,必须由程序员显式强转为对应类型的指针 - 失败时的处理:内存不足时,
new默认抛出std::bad_alloc异常;malloc会返回NULL空指针
3 全局/静态变量的分区细节¶
| 变量类型 | 分区 | 初始化时机 |
|---|---|---|
int g = 10; |
数据段 | 程序启动时,值写入 |
int g;(未初始化) |
BSS 段 | 程序启动时 自动清零 |
static int s = 5; |
数据段 | 首次执行到之前已初始化 |
const int c = 3; |
只读段 | 程序启动时,不可修改 |
BSS 段为什么不占用可执行文件空间
.data 段要存每个变量的 实际初值,所以占用文件空间;而 .bss 段的变量全是 0,只需记录"这块区域有多大",加载时由操作系统统一清零即可,不占用可执行文件体积。这就是 int a[1000000]; 全局数组(未初始化)不会让 exe 变大 4MB 的原因
常见问题
| 问题 | 原因 |
|---|---|
| 栈溢出 | 递归过深、局部大数组过大 |
| 内存泄漏 | new 后忘 delete,堆越用越多 |
| 悬垂指针 | 指向已释放内存的指针再次使用 |
| 段错误(Segfault) | 修改只读段(如字符串字面量)、访问越界地址 |
| 堆损坏 | 重复 delete、越界写堆内存 |
内存对齐(Memory Alignment)
既然有了基本数据类型,为什么结构体的大小往往比内部元素大小的总和要大?
物理硬件的限制。CPU 访问内存时并不是按字节逐个读取的,而是按字(Word,比如 32 位机器一次读 4 字节,64 位一次读 8 字节)成块读取的。如果一个 int 放到了不对齐的地址里,CPU 可能需要读两次内存然后再进行高低位拼凑,极大地损耗性能。甚至某些硬件平台不支持非对齐访问,会直接抛出硬件异常
对齐规则:
- 结构体的第一个成员放在偏移量为 0 的位置
- 之后每个成员放在其自身类型大小的整数倍的偏移地址处(如果有内部子结构体,要看子结构体里最大成员的大小)
- 整个结构体的总大小必须是其最大基本数据类型成员大小的整数倍,不足的在末尾填充字节(Padding)
可以通过添加 #pragma pack(n) 宏来强制改变编译器的对齐字节数,或者在 C++11 后使用 alignas(n)
评论区
欢迎在评论区指出文档错误,为文档提供宝贵意见,或写下你的疑问