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

本页目录

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

很多 C++ 内存错误并不来自手写 new/delete。

它们来自容器内部搬家之后,你还拿着旧地址、旧引用、旧迭代器。

1. vector 的对象本体和元素缓冲区 ​

看代码:

cpp
#include <vector>

int main() {
    std::vector<int> values{1, 2, 3};
}
1
2
3
4
5

values 这个对象本体通常在当前作用域中。

但元素缓冲区通常是动态分配的。

text
stack frame
+------------------------------+
| values object                |
|  begin ----+                 |
|  end   ----|----+            |
|  cap   ----|----|----+       |
+------------|----|----|-------+
             |    |    |
             v    v    v
heap       +------------------+
           | 1 | 2 | 3 | ...  |
           +------------------+
1
2
3
4
5
6
7
8
9
10
11
12

vector 的核心承诺是元素连续存放。

这让它访问很快,也适合和 C API 交互。

但连续存放也带来一个问题:容量不够时,原地扩展不一定可能。

2. 扩容时发生什么 ​

看代码:

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

如果后面的 push_back 导致扩容,过程大致是:

text
old buffer
+-----+
|  1  |
+-----+
   ^
   |
   p

vector needs more capacity
  |
  v
allocate new buffer
  |
  v
move/copy old elements
  |
  v
destroy old elements
  |
  v
release old buffer

new buffer
+-----+-----+-----+-----+
|  1  |  2  |  3  |  4  |
+-----+-----+-----+-----+

p still points to old released buffer
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

所以 values 还活着,不代表 p 还有效。

3. 引用和迭代器也会失效 ​

不仅指针会失效。

引用也会:

cpp
std::vector<int> values{1, 2, 3};
int& first = values[0];

values.push_back(4);

first = 10; // 如果扩容发生,可能危险
1
2
3
4
5
6

迭代器也会:

cpp
auto it = values.begin();
values.push_back(5);
// it 可能已经失效
1
2
3

迭代器可以理解成容器里的“位置对象”。

但它不是永久门牌号。

容器内部结构变化后,旧位置对象可能不再合法。

4. reserve 能减少扩容 ​

如果你提前知道大概元素数量,可以用:

cpp
std::vector<int> values;
values.reserve(1000);
1
2

reserve 的意思是:

text
提前准备至少能容纳这么多元素的容量。
1

它不改变 size(),只影响 capacity()。

cpp
std::vector<int> values;
values.reserve(10);

std::cout << values.size() << '\n';     // 0
std::cout << values.capacity() << '\n'; // 至少 10
1
2
3
4
5

reserve 不能让所有失效问题消失。

但在批量插入场景,它能减少重新分配次数,也能减少旧指针失效的机会。

5. string 也有类似问题 ​

std::string 也可能管理动态字符缓冲区。

cpp
#include <string>

std::string s = "abc";
const char* p = s.c_str();

s += "defghijklmnopqrstuvwxyz";

// p 可能已经失效
1
2
3
4
5
6
7
8

c_str() 返回的是指向字符串内部字符存储的指针。

只要字符串修改导致重新分配,旧指针就不能继续依赖。

正确习惯是:

text
不要长期保存 string 内部指针。
需要时临时获取。
修改 string 后重新获取。
1
2
3

6. span 和 string_view 是非拥有视图 ​

std::span<T> 表示一段连续对象的视图。

std::string_view 表示一段字符的视图。

它们通常只保存:

text
起始地址
长度
1
2

它们不拥有底层对象。

所以:

cpp
#include <string>
#include <string_view>

std::string_view bad() {
    std::string s = "hello";
    return s;
}
1
2
3
4
5
6
7

返回后 s 已经析构。

string_view 只剩旧地址和长度。

它不会让字符串继续活着。

7. map/list/deque 的失效规则不同 ​

不同容器的内部结构不同。

所以失效规则也不同。

学习阶段可以先记住方向:

text
vector/string
  连续存储,扩容会搬家,旧指针/引用/迭代器容易失效

deque
  分段连续,规则比 vector 复杂

list
  节点式结构,插入删除通常不搬其他节点,但局部性差

map/unordered_map
  节点或桶结构,插入删除和 rehash 会影响迭代器规则
1
2
3
4
5
6
7
8
9
10
11

不要靠感觉猜。

写重要代码时,要查具体容器的 iterator invalidation 规则。

8. 更稳的写法 ​

少长期保存内部地址:

cpp
// 容易出问题
int* p = &values[0];
modify(values);
use(*p);
1
2
3
4

改成保存下标:

cpp
std::size_t index = 0;
modify(values);
use(values[index]);
1
2
3

保存下标也不是万能。

如果元素被删除,或者顺序改变,下标含义也会变。

但它至少不会指向释放后的旧缓冲区。

另一种方式是把对象放进稳定拥有结构中:

cpp
std::vector<std::unique_ptr<Widget>> widgets;
1

vector 扩容时会移动 unique_ptr 对象。

但每个 Widget 自身仍在各自的动态存储位置上。

这可以让指向 Widget 的裸观察指针更稳定一些。

不过它也带来额外动态分配和缓存局部性成本。

9. 本篇总结 ​

容器内存问题的核心是:

text
容器对象活着
  !=
旧指针活着
  !=
旧引用有效
  !=
旧迭代器有效
1
2
3
4
5
6
7

判断时问:

text
容器是否可能重新分配?
元素是否可能被移动?
旧位置对象是否仍然有效?
我保存的是 owner,还是内部观察入口?
1
2
3
4

容器是 RAII 的好例子。

它帮你释放资源。

但它不能保证你保存的每个旧地址永远有效。

最后更新于:

Pager
上一篇7. 智能指针与所有权关系:谁拥有,谁只是看 / Smart Pointers and Ownership Graphs
下一篇9. allocator 与 PMR:把分配策略抽出来 / Allocators and PMR

持续记录,持续成长

Copyright © Tidenflow