Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

内存管理 / Memory Management

1. C++ 内存管理总览:先看完整地图 / C++ Memory Management Map

2. 程序内存布局:内存不是只有堆和栈 / Program Memory Layout

3. 栈与函数调用:局部变量为什么会消失 / Stack and Function Calls

4. 堆与动态分配:new 到底做了什么 / Heap and Dynamic Allocation

5. 对象生命周期:地址还在不代表对象还活着 / Object Lifetime

6. RAII 与所有权:让对象负责清理 / RAII and Ownership Design

7. 智能指针与所有权关系:谁拥有,谁只是看 / Smart Pointers and Ownership Graphs

8. 容器、迭代器与内存失效 / Containers, Iterators, and Invalidation

9. allocator 与 PMR:把分配策略抽出来 / Allocators and PMR

10. 内存错误与 Sanitizer:把崩溃变成证据 / Memory Errors and Sanitizers

本页目录

C++ 内存管理总览:先看完整地图 / C++ Memory Management Map ​

本文是内存管理模块的入口。

这里不把内存管理讲成“栈快、堆慢、new 要 delete”。我们先建立一张完整地图:程序有哪些内存区域,对象什么时候算活着,谁负责释放,为什么指针还在但对象已经没了。

内存管理不是一组孤立规则。

如果只背规则,很容易得到这些碎片:

text
+----------------+
| stack          |
+----------------+
| heap           |
+----------------+
| new / delete   |
+----------------+
| smart pointer  |
+----------------+
| RAII           |
+----------------+
1
2
3
4
5
6
7
8
9
10
11

这些词都重要,但它们处在不同层次。

栈和堆是在说“存储大概来自哪里”。

生命周期是在说“对象什么时候可以被合法访问”。

所有权是在说“谁负责清理资源”。

指针、引用、span、string_view 是在说“我怎样观察或访问对象”。

RAII、容器、智能指针是在说“怎样让类型替我们管理清理责任”。

把这些层次分开,后面学 new、delete、unique_ptr、shared_ptr、allocator、Sanitizer 才不会变成背术语。

1. 内存管理到底在管理什么 ​

一个 C++ 程序运行时,至少有四件事需要同时考虑:

text
storage
  一片可用字节从哪里来

object
  某个类型的对象是否已经在那片字节上开始存在

lifetime
  对象从什么时候到什么时候可以合法使用

ownership
  谁负责在最后结束对象并释放相关资源
1
2
3
4
5
6
7
8
9
10
11

看一段最普通的代码:

cpp
#include <string>

void f() {
    std::string name = "Alice";
}
1
2
3
4
5

它背后不是“有一个字符串变量”这么简单。

可以拆成:

text
进入 f()
  |
  v
为局部对象 name 准备存储
  |
  v
调用 std::string 构造函数
  |
  v
name 的生命周期开始
  |
  v
函数体执行
  |
  v
离开 f()
  |
  v
调用 std::string 析构函数
  |
  v
name 的生命周期结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

std::string 这个对象本体可能在函数栈帧里。

但它内部管理的字符缓冲区可能在动态存储区里。

这说明“变量在哪里”经常不是一个问题,而是两个问题:

text
对象本体在哪里?
对象内部拥有的资源在哪里?
1
2

2. 程序内存不是只有堆和栈 ​

从学习角度,一个进程的地址空间可以先粗略看成这样:

text
+--------------------------------------------------+
| text                                             |
| 编译后的机器指令                                  |
+--------------------------------------------------+
| rodata                                           |
| 字符串字面量、只读常量                            |
+--------------------------------------------------+
| data / bss                                       |
| 全局对象、static 对象                             |
+--------------------------------------------------+
| heap                                             |
| 动态分配:new、malloc、容器扩容                   |
+--------------------------------------------------+
| mmap / shared libraries                          |
| 动态库、内存映射文件、线程栈等                     |
+--------------------------------------------------+
| stack                                            |
| 函数调用帧、局部自动对象、返回地址                 |
+--------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这张图不是让你背平台细节。

它只要说明:

text
内存是总概念。
堆和栈只是其中两类区域。
1
2

看代码:

cpp
int global_value = 10;

int main() {
    int local = 42;
    const char* text = "hello";
    int* p = new int(7);
    delete p;
}
1
2
3
4
5
6
7
8

大致对应:

text
global_value
  -> 全局/静态区域

local
  -> main() 调用帧中的局部对象

"hello"
  -> 通常在只读数据区域

text 这个指针变量
  -> main() 调用帧中

new int(7) 创建的 int 对象
  -> 动态存储区,也就是口语里的堆

p 这个指针变量
  -> main() 调用帧中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

注意最后两项。

p 这个变量本身和 p 指向的对象不是一回事。

指针变量可以在栈上,指向堆对象。

也可以在堆对象内部,指向另一个对象。

也可以是全局变量,指向静态对象。

所以以后不要把“用了指针”直接等同于“用了堆”。

3. 指针还在,不等于对象还在 ​

看代码:

cpp
int* p = nullptr;

{
    int value = 42;
    p = &value;
}

// 这里 p 还保存着一个地址
// 但 value 的生命周期已经结束
1
2
3
4
5
6
7
8
9

这段代码最容易卡住的地方是:

text
p 明明还有值,为什么不能用?
1

因为指针保存的是地址数字。

对象生命周期结束时,地址数字不会自动从所有指针里消失。

真正的问题是:

text
那个地址上已经没有一个活着的 int 对象。
1

所以内存安全不是只看“指针是不是空”。

还要看:

text
指针指向的对象是否仍然活着?
这段代码有没有权利访问它?
1
2

4. 所有权不是“谁拿着地址” ​

再看:

cpp
int* p = new int(42);
int* q = p;
1
2

p 和 q 保存相同地址。

但不能说它们都拥有这个对象。

如果这样写:

cpp
delete p;
delete q;
1
2

就是重复释放。

更准确的关系是:

text
dynamic int object
  ^
  |
  +-- p:应该被视为唯一清理责任人
  |
  +-- q:只是另一个观察入口
1
2
3
4
5
6

裸指针 int* 本身表达不清这个关系。

现代 C++ 更希望你用类型表达:

text
std::unique_ptr<T>
  独占拥有,一个对象只有一个 owner

std::shared_ptr<T>
  共享拥有,最后一个 owner 离开时清理

std::weak_ptr<T>
  观察 shared_ptr 管理的对象,但不增加拥有计数

T*
  可以是观察,也可以是历史代码里的拥有者,需要额外约定

T&
  引用一个已经存在的对象,通常不表达拥有

std::span<T>
  观察一段连续对象,不拥有

std::string_view
  观察一段字符,不拥有
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这就是为什么 C++ 内存管理不只是“释放内存”。

它首先是接口设计问题。

5. new 在这张地图里的位置 ​

new 很重要,但不是内存管理的全部。

new int(42) 至少做两件事:

text
申请足够放 int 的动态存储
  |
  v
在那块存储上构造 int 对象
  |
  v
返回对象地址
1
2
3
4
5
6
7

delete p 反过来:

text
调用对象析构逻辑
  |
  v
释放动态存储
1
2
3
4

对 int 来说析构几乎看不出来。

对 std::string、std::vector、文件对象、锁对象来说,析构就非常重要。

普通代码里,我们更希望这样写:

cpp
#include <memory>
#include <string>

auto p = std::make_unique<std::string>("hello");
1
2
3
4

因为 unique_ptr 把清理责任写进了类型里。

6. RAII 是 C++ 内存管理的主线 ​

RAII 的全称是 Resource Acquisition Is Initialization。

直译很拗口,可以先理解成:

text
让对象生命周期管理资源生命周期。
1

资源不只是内存。

还包括:

text
文件句柄
网络连接
互斥锁
数据库连接
图形资源
临时目录
1
2
3
4
5
6

没有 RAII 的代码经常长这样:

text
申请资源
  |
  v
做第一步
  |
  v
做第二步
  |
  +-- 中途 return
  +-- 中途抛异常
  +-- 中途忘记释放
  |
  v
手动释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14

RAII 把它改成:

text
构造对象时获得资源
  |
  v
使用对象
  |
  v
对象离开作用域时析构
  |
  v
析构函数释放资源
1
2
3
4
5
6
7
8
9
10

这就是为什么 std::vector、std::string、std::unique_ptr、std::ifstream、std::lock_guard 都是学习内存管理的重要例子。

它们不只是“库类型”。

它们体现了 C++ 管资源的主要方法。

7. 容器内存必须单独学 ​

很多内存问题不来自你手写 new。

它来自容器内部重新分配。

看例子:

cpp
#include <vector>
#include <iostream>

int main() {
    std::vector<int> values;
    values.push_back(1);

    int* p = &values[0];

    values.push_back(2);
    values.push_back(3);
    values.push_back(4);

    std::cout << *p << '\n'; // 可能已经危险
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

vector 为了保持元素连续,容量不够时可能申请一块更大的缓冲区,把元素搬过去,再释放旧缓冲区。

text
before reallocation

values object
  |
  v
old buffer: [1]
             ^
             |
             p

after reallocation

values object
  |
  v
new buffer: [1][2][3][4]

old buffer released
  ^
  |
  p still points here
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

所以 values 对象还活着,不代表旧指针、旧引用、旧迭代器还有效。

这类问题必须作为内存管理的一部分来学。

8. 诊断工具不是附录 ​

C++ 的一部分内存错误属于未定义行为。

“未定义行为”不是“随机行为”这么简单。

它的意思是:一旦程序违反语言规则,标准不再保证后续结果。

程序可能崩溃,可能看似正常,可能在优化后变得完全不同。

所以学习内存管理一定要学工具。

常用编译方式:

bash
g++ -std=c++17 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.cpp -o app
1

这些参数的作用是:

text
-g
  生成调试信息,错误报告里能看到源码行

-O1
  开一点优化,同时保留较好的检查效果

-fsanitize=address
  打开 AddressSanitizer,检查常见内存访问错误

-fsanitize=undefined
  打开 UndefinedBehaviorSanitizer,检查一部分未定义行为

-fno-omit-frame-pointer
  保留帧指针,让调用栈更容易读
1
2
3
4
5
6
7
8
9
10
11
12
13
14

工具不会证明程序一定正确。

但它能把很多“感觉不对”变成可读证据。

9. 本模块阅读路线 ​

建议按这个顺序读:

text
00 总览
  |
  v
01 程序内存布局
  |
  v
02 栈与函数调用
  |
  v
03 堆与动态分配
  |
  v
04 对象生命周期
  |
  v
05 RAII 与所有权
  |
  v
06 智能指针
  |
  v
07 容器与内存失效
  |
  v
08 allocator 与 pmr
  |
  v
09 内存错误与工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

如果你暂时不想学高级部分,可以先读到 07。

08 allocator 与 pmr 更偏工程优化和库设计,可以第二轮再读。

10. 本篇总结 ​

以后看到任何内存相关代码,先问这几个问题:

text
1. 这里的对象是谁?
2. 它的存储来自哪里?
3. 它的生命周期什么时候开始?
4. 它的生命周期什么时候结束?
5. 谁拥有它?
6. 谁只是观察它?
7. 观察者会不会活得比拥有者更久?
8. 失败路径上是否仍然清理?
1
2
3
4
5
6
7
8

这八个问题比“在栈上还是堆上”更完整。

堆和栈当然要学。

但它们只是入口,不是全部。

最后更新于:

Pager
上一篇← C++ 编程 / C++ Programming
下一篇2. 程序内存布局:内存不是只有堆和栈 / Program Memory Layout

持续记录,持续成长

Copyright © Tidenflow