编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

      • README
      • Cpp简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针引用
      • 类和对象
      • 继承多态
      • 内存模型
        • 目录介绍
        • 01.学习目标
        • 02.内存五大区域
          • 2.1 五区模型总览
          • 2.2 代码区(Text)
          • 2.3 全局/静态区(Data + BSS)
          • 2.4 堆区(Heap)
          • 2.5 栈区(Stack)
          • 2.6 常量区(rodata)
        • 03.五区在汇编层的验证
          • 3.1 用 nm / objdump 查看变量所在段
          • 3.2 用一段代码打印各类变量地址
        • 04.变量的三大属性:作用域 / 存储期 / 链接性
          • 4.1 作用域(Scope)
          • 4.2 存储期(Storage Duration)
          • 4.3 链接性(Linkage)
          • 4.4 三者的组合关系矩阵
        • 05.栈帧机制:函数调用如何利用栈
        • 06.堆分配:new/malloc 究竟在做什么
        • 07.对象的生命周期窗口
          • 7.1 局部对象:随作用域结束而析构
          • 7.2 静态对象:贯穿程序生命
          • 7.3 堆对象:必须手动释放(或交给智能指针)
        • 08.六个经典内存错误
          • 8.1 返回局部变量的指针/引用
          • 8.2 use-after-free(释放后使用)
          • 8.3 double-free(重复释放)
          • 8.4 内存泄漏(Memory Leak)
          • 8.5 数组越界(Buffer Overflow)
          • 8.6 栈溢出(Stack Overflow)
        • 09.与卷三 / 卷四的衔接
        • 10.本章新手陷阱 Top 5
          • 陷阱 1:以为 static 局部变量在"栈"上
          • 陷阱 2:返回局部变量的指针
          • 陷阱 3:以为字符串字面量可改
          • 陷阱 4:在栈上放超大数组
          • 陷阱 5:忘记每个 new[] 配 delete[]
        • 11.思考题
      • 动态内存
      • IO和文件
      • 异常处理
      • 线程和锁
      • STL模版
      • 预处理器
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 入门教程
杨充
2026-05-07
目录

内存模型

# 第 11 章 C++ 内存模型

本章定位:承上启下——前面学的"指针 / 引用 / 局部变量"为什么能那样工作?后面要学的"类与对象 / 动态内存 / RAII"建立在什么之上?答案是:进程的内存布局 与 变量的存储期 + 作用域 + 链接性。


# 目录介绍

  • 01.学习目标
  • 02.内存五大区域
    • 2.1 五区模型总览
    • 2.2 代码区(Text)
    • 2.3 全局/静态区(Data + BSS)
    • 2.4 堆区(Heap)
    • 2.5 栈区(Stack)
    • 2.6 常量区(rodata)
  • 03.五区在汇编层的验证
    • 3.1 用 nm / objdump 查看变量所在段
    • 3.2 用一段代码打印各类变量地址
  • 04.变量的三大属性:作用域 / 存储期 / 链接性
    • 4.1 作用域(Scope)
    • 4.2 存储期(Storage Duration)
    • 4.3 链接性(Linkage)
    • 4.4 三者的组合关系矩阵
  • 05.栈帧机制:函数调用如何利用栈
  • 06.堆分配:new/malloc 究竟在做什么
  • 07.对象的生命周期窗口
  • 08.六个经典内存错误
  • 09.与卷三 / 卷四的衔接
  • 10.本章新手陷阱 Top 5
  • 11.思考题
  • 12.推荐阅读

# 01.学习目标

  • [ ] 能画出一个 C++ 进程运行时的内存图(5 区)
  • [ ] 区分四种存储期:自动 / 静态 / 线程 / 动态
  • [ ] 区分内部链接、外部链接、无链接
  • [ ] 解释"返回局部变量引用"为什么是未定义行为
  • [ ] 知道一次 new 与一次栈对象创建在性能上为什么差几个数量级

# 02.内存五大区域

# 2.1 五区模型总览

当 C++ 程序被加载到内存运行时,操作系统给进程的"地址空间"长这样(64 位 Linux):

高地址  ┌────────────────────────┐
        │   内核空间(不可访问)    │
        ├────────────────────────┤  ← 0x7fff...(用户态上限)
        │     栈区 (Stack)         │  ↓ 向下生长
        │  · 局部变量 / 参数 / 返回地址│
        ├────────────────────────┤
        │   ↕ 中间是大量未映射区域  │
        ├────────────────────────┤
        │     堆区 (Heap)          │  ↑ 向上生长
        │  · new / malloc 分配      │
        ├────────────────────────┤
        │  全局/静态区 (BSS + Data) │
        │  · 全局变量、static 变量  │
        ├────────────────────────┤
        │  常量区 (rodata)         │
        │  · 字符串字面量、const   │
        ├────────────────────────┤
        │  代码区 (Text)           │
        │  · 编译后的机器指令       │
低地址  └────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2.2 代码区(Text)

  • 存什么:编译后的机器指令(函数体)
  • 特点:只读、可共享(同一程序多进程共享)
  • 写它会怎样:段错误(Segmentation Fault)

# 2.3 全局/静态区(Data + BSS)

  • 存什么:全局变量、static 变量
  • 细分:
    • .data:已初始化的(如 int g = 42;)
    • .bss:未初始化的(如 int g;,编译器零初始化)
  • 生命周期:从程序启动到程序结束
int  g_initialized = 42;     // .data
int  g_uninitialized;        // .bss(自动 0)
static int g_static = 100;   // .data,文件内可见
1
2
3

# 2.4 堆区(Heap)

  • 存什么:new / malloc 分配的对象
  • 特点:手动管理、空间大、慢(要找空闲块、对齐、更新元数据)
  • 释放方式:必须 delete / free,或交给智能指针 / 容器

# 2.5 栈区(Stack)

  • 存什么:局部变量、函数参数、返回地址
  • 特点:自动管理、极快(移动栈指针即可)、容量小(默认 8 MB)

# 2.6 常量区(rodata)

const char* s1 = "hello";    // "hello" 在 .rodata
char        s2[] = "hello";  // 拷贝到栈
s1[0] = 'H';                 // ❌ 段错误(修改 rodata)
s2[0] = 'H';                 // ✅ 修改栈拷贝,OK
1
2
3
4

# 03.五区在汇编层的验证

# 3.1 用 nm / objdump 查看变量所在段

// memmap.cpp
int   g_init = 42;       // .data
int   g_bss;             // .bss
const char* g_ro = "hi"; // 字符串在 .rodata
static int g_static = 1; // .data

int main() {
    int local = 5;
    static int s_local = 7;
    int* heap = new int(9);
    delete heap;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
g++ -g -c memmap.cpp -o memmap.o
nm memmap.o | sort
1
2

输出(节选):

0000000000000000 D g_init        # D = .data
0000000000000004 d g_static      # d = .data, 内部链接
0000000000000000 B g_bss         # B = .bss
0000000000000000 R g_ro          # R = .rodata
0000000000000000 T main          # T = .text
1
2
3
4
5

# 3.2 用一段代码打印各类变量地址

#include <iostream>
int   g_init = 42;
int   g_bss;
const char* g_str = "hello";
static int g_static = 1;

void f() {
    int      local  = 5;
    static int s_local = 7;
    int*     heap   = new int(9);
    std::cout << "栈    local        : " << &local    << "\n";
    std::cout << "全局  s_local      : " << &s_local  << "\n";
    std::cout << "堆    *heap        : " << heap      << "\n";
    delete heap;
}

int main() {
    std::cout << "代码  &main        : " << reinterpret_cast<void*>(&main) << "\n";
    std::cout << "全局  g_init       : " << &g_init   << "\n";
    std::cout << "全局  g_bss        : " << &g_bss    << "\n";
    std::cout << "常量  g_str → \"\"  : " << static_cast<const void*>(g_str) << "\n";
    f();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

典型输出(地址因运行环境而异):

代码  &main        : 0x000055c4...3160   ← 低(.text)
常量  g_str → ""   : 0x000055c4...3008   ← 略高(.rodata)
全局  g_init       : 0x000055c4...4010   ← 更高(.data)
全局  g_bss        : 0x000055c4...4020   ← .bss
堆    *heap        : 0x000055c4...82a0   ← 中等(heap)
栈    local        : 0x00007ffd...e0fc   ← 高得多(stack)
1
2
3
4
5
6

# 04.变量的三大属性:作用域 / 存储期 / 链接性

理解了这三个,看任何 C++ 代码都不会迷失。

# 4.1 作用域(Scope)

变量名在源代码哪段范围内可见。

作用域 何时具有
块作用域(local) {} 局部变量、函数参数
函数作用域 仅 goto 标签
文件作用域(global) 所有函数外声明
类作用域 类成员(. / -> / :: 访问)
命名空间作用域 namespace { ... } 内

# 4.2 存储期(Storage Duration)

变量对应内存活多久——C++ 中最重要的属性。

存储期 关键字 在哪里 何时分配 / 释放
自动(automatic) 默认 栈 进入作用域分配,离开作用域释放
静态(static) static / 全局 全局区 程序启动初始化,程序结束释放
线程(thread) thread_local(C++11) TLS 线程开始分配,线程结束释放
动态(dynamic) new / malloc 堆 显式分配,显式释放

# 4.3 链接性(Linkage)

同一名字在不同源文件之间是否指向同一实体。

链接性 含义 示例
无链接 仅当前块作用域可见 函数内局部变量
内部链接 仅当前翻译单元可见 static int x;(全局)/ 匿名命名空间
外部链接 整个程序可见 普通全局变量 / 函数
// a.cpp
int g_x = 10;          // 外部链接
static int g_y = 20;   // 内部链接:仅 a.cpp 可见
namespace {
    int g_z = 30;      // 内部链接(C++11 起匿名命名空间是新的"static")
}

// b.cpp
extern int g_x;  // ✅ OK
extern int g_y;  // ❌ 链接错误:g_y 是内部链接
1
2
3
4
5
6
7
8
9
10

# 4.4 三者的组合关系矩阵

// 1. 块作用域 + 自动存储期 + 无链接(最常见)
void f()  { int x = 1; }

// 2. 块作用域 + 静态存储期 + 无链接
void g()  { static int counter = 0; ++counter; }

// 3. 文件作用域 + 静态存储期 + 内部链接
static int g_count = 0;

// 4. 文件作用域 + 静态存储期 + 外部链接
int g_total = 0;

// 5. 块作用域 + 线程存储期 + 无链接
void h()  { thread_local int tls = 0; }

// 6. 块作用域 + 动态存储期 + 无链接
void i()  { int* p = new int(42); delete p; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 05.栈帧机制:函数调用如何利用栈

int add(int a, int b) {
    int sum = a + b;
    return sum;
}
int main() { int x = add(3, 4); }
1
2
3
4
5

调用 add(3, 4) 时:

  1. 传参:把 3 / 4 放入寄存器或压栈
  2. 保存返回地址:main 下一条指令的地址压栈
  3. 跳转:CPU 指令指针指向 add 入口
  4. 建立栈帧:保存旧 RBP,把 RBP 设为当前 RSP
  5. 分配局部变量:RSP 下移给 sum 留空间
  6. 执行函数体
  7. 返回:把返回值放寄存器,恢复 RBP,跳回返回地址

栈帧布局:

高地址                                    ← main 的栈帧
        ┌──────────────────────────┐
        │  main 的局部变量 x         │
        ├──────────────────────────┤
        │  main 调用 add 的返回地址  │  ← call 指令压入
        ├──────────────────────────┤
        │  保存的旧 RBP              │  ← add 函数序言压入
        ├──────────────────────────┤  ← add 的栈帧底(新 RBP)
        │  add 的参数 a, b           │
        ├──────────────────────────┤
        │  add 的局部变量 sum         │
低地址  └──────────────────────────┘  ← 当前 RSP
1
2
3
4
5
6
7
8
9
10
11
12

栈溢出——栈空间通常 8 MB(Linux 默认;ulimit -s 查看):

void rec() { rec(); }   // 无限递归 → 栈溢出
void f()   { int huge[10'000'000]; }  // 40 MB > 8 MB → 栈溢出
1
2

正确做法:超大数据放堆(new / std::vector)。

# 06.堆分配:new/malloc 究竟在做什么

堆不是连续切——由运行时分配器(ptmalloc / tcmalloc / jemalloc)维护空闲块链表:

[ used 16B | free 32B | used 64B | free 128B | used 32B | ...]
1

一次 new Foo(1,2) 实际执行:

  1. 调用 operator new(sizeof(Foo)),遍历自由列表找够大的
  2. 分裂:返回所需大小,把剩余加回自由列表
  3. 把返回地址作为 this,调用 Foo::Foo(1,2) 构造函数
  4. 把构造好的指针赋给 p

delete p:

  1. 调用 Foo::~Foo() 析构
  2. 调用 operator delete(p),把内存归还自由列表(可能与相邻空闲块合并)

关键差距:

操作 平均耗时(量级)
创建栈对象(局部变量) ~ 1 ns(仅移动 RSP)
一次 new ~ 50-200 ns(找块 + 锁 + 构造)
一次系统调用(brk / mmap) ~ 1-10 µs

这就是为什么"性能敏感的循环里绝不要 new"。

# 07.对象的生命周期窗口

# 7.1 局部对象:随作用域结束而析构

void f() {
    std::string s = "hello";  // 构造在栈
}                              // ← 离开作用域 → s.~string() 自动调
1
2
3

# 7.2 静态对象:贯穿程序生命

struct Logger {
    Logger()  { /*...*/ }
    ~Logger() { /*...*/ }   // 程序退出时调
};
Logger& logger() {
    static Logger inst;     // 首次进入函数时构造(C++11 起线程安全)
    return inst;
}
1
2
3
4
5
6
7
8

# 7.3 堆对象:必须手动释放(或交给智能指针)

// ❌ 老 C++ 风格:容易漏 delete
Foo* p = new Foo();
// ... 中间 throw 异常或 return 都会泄漏
delete p;

// ✅ 现代 C++:智能指针自动释放
auto p = std::make_unique<Foo>();
// 不论怎么走(return / throw),离开作用域 unique_ptr 析构自动 delete
1
2
3
4
5
6
7
8

这就是 RAII(Resource Acquisition Is Initialization)的核心思想——把"资源生命周期"绑定到"栈对象的生命周期"。第 13 章会展开。

# 08.六个经典内存错误

# 8.1 返回局部变量的指针/引用

int& bad() {
    int x = 42;
    return x;     // ❌ 函数返回后 x 已析构,引用悬空
}
int main() {
    int& r = bad();
    std::cout << r;  // ❌ 未定义行为
}
1
2
3
4
5
6
7
8

# 8.2 use-after-free(释放后使用)

int* p = new int(1);
delete p;
*p = 2;   // ❌ p 已悬空
1
2
3

# 8.3 double-free(重复释放)

int* p = new int(1);
delete p;
delete p;   // ❌ 段错误或堆破坏
1
2
3

# 8.4 内存泄漏(Memory Leak)

void f() {
    int* p = new int(1);
    if (some_error) return;  // ❌ 漏 delete,每次调用泄漏 4 字节
    delete p;
}
1
2
3
4
5

# 8.5 数组越界(Buffer Overflow)

int arr[5] = {1,2,3,4,5};
arr[5] = 10;   // ❌ 越界一格
arr[100] = 99; // ❌ 越界很多 → 可能写坏栈帧 / 返回地址
1
2
3

# 8.6 栈溢出(Stack Overflow)

void f() { int huge[10'000'000]; }
1

卷四第 5、6 章会教你用 AddressSanitizer / Valgrind 自动检出上面六类错误中的绝大多数。

# 09.与卷三 / 卷四的衔接

本章涉及 卷三深挖 卷四实战
五区内存模型 第 1 章 C++ 内存模型与对象内存布局 —
栈帧 第 4 章 构造析构与对象生命周期 第 1 章 BusError 排查
堆分配器 第 1 章(cache line / NUMA) 第 11 章 缓存友好编程
RAII 第 10 章 智能指针与资源管理 RAII 第 5 章 内存泄漏排查
use-after-free — 第 8 章 安全漏洞与防御

# 10.本章新手陷阱 Top 5

# 陷阱 1:以为 static 局部变量在"栈"上

void f() {
    static int x = 0;   // ❌ 不在栈,在 .data 全局区
    ++x;
}
1
2
3
4

# 陷阱 2:返回局部变量的指针

const char* msg() {
    char buf[32];
    sprintf(buf, "hi");
    return buf;   // ❌ buf 出函数即销毁
}
1
2
3
4
5

# 陷阱 3:以为字符串字面量可改

char* s = "hello";   // C++11 后甚至编译警告
s[0] = 'H';          // ❌ 段错误(写 .rodata)
1
2

# 陷阱 4:在栈上放超大数组

void f() {
    int img[1024 * 1024 * 4];   // 16 MB → 栈溢出
}
1
2
3

正解:std::vector<int> img(1024 * 1024 * 4);(vector 内部用堆)。

# 陷阱 5:忘记每个 new[] 配 delete[]

int* arr = new int[100];
delete arr;     // ❌ 必须 delete[],否则只析构第一个 + 错误的释放
delete[] arr;   // ✅
1
2
3

# 11.思考题

  1. 写一段代码,分别打印一个全局变量、一个静态局部变量、一个堆变量、一个栈变量的地址,画出它们在地址空间中的相对位置。
  2. static 关键字在 C 和 C++ 中分别有几种含义?
  3. 说出 5 种"无链接"的变量场景。
  4. 解释为什么"递归过深"会导致 SIGSEGV 而不是程序"慢死"。
  5. int* p = new int[5]; 后,调用 delete p; 而不是 delete[] p; 会发生什么?
  6. 一个 64 位 Linux 进程理论上能用多少地址空间?实际能 new 多少?
  7. 写一段代码,故意制造内存泄漏,然后用 valgrind --tool=memcheck ./a.out 验证。
  8. 为什么栈分配比堆快?给出三条理由。
  9. thread_local 变量与 static 变量在内存中的区别是什么?
  10. 如果你要实现一个 1ms 内必须返回的实时函数,你会怎样避免里面任何 new / malloc?
上次更新: 2026/06/10, 11:13:41
继承多态
动态内存

← 继承多态 动态内存→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式