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

STL 与泛型编程 / STL & Generic Programming

1. STL 与泛型编程路线 / STL And Generic Programming Roadmap

2. 容器与所有权模型 / Containers And Ownership Model

3. 迭代器、Range 与 View 模型 / Iterator, Range, And View Model

4. 算法与 Ranges 管道 / Algorithms And Ranges Pipelines

5. 模板、Traits 与 Concepts:面向库设计的泛型约束 / Templates, Traits, And Concepts For Library Design

6. 可调用对象、函数对象与定制点 / Callables, Function Objects, And Customization

7. Allocator、PMR 与容器内存资源 / Allocators, PMR, And Container Memory

8. STL 调试、性能与实践项目 / STL Debugging, Performance, And Practice

本页目录

容器与所有权模型 / Containers And Ownership Model ​

本文属于 C++ STL 与泛型编程模块。

在本模块里,它的位置是:第一篇专题,先解决数据放在哪里以及由谁拥有。

容器选择不是背 vector、list、map 的优缺点。它首先是一个所有权问题:对象由谁保存、生命周期跟谁走、外部能否持有引用、修改后什么会失效。

1. 为什么这个主题重要 ​

容器一旦选错,后面算法、迭代器、性能和并发都会跟着别扭。很多 C++ 崩溃不是算法写错,而是保存了容器内部引用,然后容器修改导致引用失效。

text
业务数据
  |
  v
容器选择
  |
  +-- 生命周期
  +-- 内存布局
  +-- 访问复杂度
  +-- 迭代器失效
  +-- 缓存局部性
1
2
3
4
5
6
7
8
9
10

如果只背 API 名字,很容易出现一种学习错觉:

text
看过很多函数
  |
  v
写代码时还是不知道选哪个
  |
  v
出错后只会换 API
  |
  v
没有形成工程判断
1
2
3
4
5
6
7
8
9
10

本篇的目标不是把所有细节一次塞满,而是让你建立一个能迁移的判断框架。

2. 最小心智模型 ​

text
对象集合
  |
  +-- 谁拥有元素?
  +-- 元素地址是否稳定?
  +-- 访问模式是什么?
  +-- 修改频率在哪里?
  +-- 是否需要排序或哈希?
  |
  v
容器选择
1
2
3
4
5
6
7
8
9
10

先把这个模型拿住。

后面所有 API、类型、工具和错误,都可以放回这张图里理解。

学习时反复问:

text
这个对象拥有什么?
这个接口暴露什么?
这个操作会不会失效?
这个抽象在编译期还是运行期生效?
这个选择影响正确性、性能还是可维护性?
1
2
3
4
5

3. 本文基础词小注释 ​

ownership:所有权。谁负责保存对象并决定对象什么时候销毁。容器通常拥有其中的元素。

contiguous storage:连续存储。元素在内存中连续排列,例如 vector、array、string,缓存友好。

node-based container:节点容器。元素分散在独立节点中,例如 list、map,引用稳定性和插入删除规则不同。

iterator invalidation:迭代器失效。容器修改后,原来指向元素的迭代器、引用或指针不再可用。

complexity:复杂度。操作随数据规模增长的成本,例如 O(1)、O(log n)、O(n)。

cache locality:缓存局部性。连续访问相邻内存通常更快,因为 CPU cache 能批量加载附近数据。

4. 机制:从表面 API 看到工程边界 ​

容器的底层布局决定了它的工程行为。vector 扩容可能搬家;map 节点地址通常更稳定;unordered_map 查找平均快但依赖哈希质量;deque 分段连续,适合两端操作但不等同于 vector。

text
vector
  [a][b][c][d]
  连续,扩容可能整体搬迁

list
  [a] -> [b] -> [c]
  节点分散,遍历局部性差

map
  tree nodes
  有序,查找 O(log n)

unordered_map
  buckets -> nodes
  平均查找快,顺序不稳定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

机制层最重要的是边界感。

一个 C++ 抽象通常同时影响三件事:

text
源码表达
  代码看起来怎样写

编译期约束
  类型、模板、重载、concept 是否能阻止错误

运行时行为
  内存、迭代、分配、同步、系统调用或 I/O 成本
1
2
3
4
5
6
7
8

好的学习顺序是:先知道这个抽象解决什么问题,再看它提供什么接口,最后看它在哪些条件下会失效。

5. Step-by-Step Examples ​

1. vector 默认优先 ​

大多数普通序列数据,vector 是第一候选。

cpp
#include <vector>

struct Particle {
    double x{};
    double y{};
    double z{};
};

std::vector<Particle> particles;
particles.reserve(1000);
particles.push_back({1.0, 2.0, 3.0});
1
2
3
4
5
6
7
8
9
10
11

拆开看:

text
reserve
  提前申请容量,减少扩容搬迁

push_back
  在尾部追加元素

vector<Particle>
  容器拥有 Particle 对象
1
2
3
4
5
6
7
8

这个例子要观察的不是代码长度,而是边界:

text
连续存储、尾部追加、批量遍历都适合 vector
1

2. 保存 ID 而不是裸指针 ​

如果外部要长期引用容器元素,优先考虑稳定 ID 或索引策略。

cpp
#include <optional>
#include <vector>

struct Node {
    int id{};
    double value{};
};

std::optional<Node> find_by_id(const std::vector<Node>& nodes, int id) {
    for (const auto& node : nodes) {
        if (node.id == id) return node;
    }
    return std::nullopt;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

拆开看:

text
id
  业务稳定标识

optional<Node>
  表达可能找不到

返回副本
  避免外部持有容器内部悬空引用
1
2
3
4
5
6
7
8

这个例子要观察的不是代码长度,而是边界:

text
接口设计可以规避容器修改后的引用风险
1

6. 工程取舍 ​

容器选择要先看访问模式。尾部追加和遍历优先 vector;频繁按 key 查找考虑 unordered_map 或 map;需要排序顺序考虑 map 或排序 vector;需要稳定地址不代表一定选 list,可能更适合 vector + index、deque、unique_ptr 或对象池。

text
遍历多
  vector

按 key 查找多
  unordered_map / map

有序遍历
  map / sorted vector

两端插入删除
  deque

需要稳定引用
  先问能否改成 ID,不要本能选 list
1
2
3
4
5
6
7
8
9
10
11
12
13
14

取舍不是一句“看情况”。

你要把“情况”具体化:

text
数据规模
访问模式
生命周期
错误代价
性能瓶颈
团队维护成本
平台限制
测试和诊断能力
1
2
3
4
5
6
7
8

7. 常见错误 ​

1. 本能选择 list ​

text
中间插入删除
  |
  v
选择 list
  |
  v
实际瓶颈变成遍历和分配
1
2
3
4
5
6
7

先测真实场景。很多时候 vector 搬移成本低于 list 的缓存损失。

2. 保存 vector 元素指针 ​

text
auto* p = &values[0]
values.push_back(...)
*p
  |
  v
可能悬空
1
2
3
4
5
6

使用索引、ID、reserve、稳定容器或重查找。

3. 忽略 unordered_map 最坏情况 ​

text
哈希冲突
  |
  v
查找退化
  |
  v
性能尖刺
1
2
3
4
5
6
7

关注哈希质量、负载因子、输入是否可控。

8. 调试、验证和性能观察 ​

容器问题可以用 AddressSanitizer、Debug iterator、断点观察 capacity 和元素地址变化来定位。性能问题可以记录分配次数、遍历耗时和 cache miss。

text
崩溃
  ASan / debug iterator

性能慢
  benchmark / profiler / allocation count

顺序不对
  检查 map/unordered_map 语义

引用失效
  观察 capacity、erase、insert、rehash
1
2
3
4
5
6
7
8
9
10
11

验证要尽量小。

一次只验证一个假设:

text
固定输入
  |
  v
固定构建命令
  |
  v
只改变一个实现选择
  |
  v
记录结果
  |
  v
判断假设是否成立
1
2
3
4
5
6
7
8
9
10
11
12
13

9. 实践任务 ​

练习 1:设计容器选择矩阵 ​

为日志、用户表、粒子数组、任务队列分别选容器。

text
列访问模式
列修改模式
列生命周期要求
选择容器
写风险
1
2
3
4
5

练习 2:观察 vector 扩容 ​

打印 data() 地址和 capacity。

text
push_back 循环
每次 capacity 变化时打印地址
理解扩容搬迁
1
2
3

练习 3:比较 map 和 unordered_map ​

构造一组 key,比较顺序和查找方式。

text
插入相同数据
遍历输出
观察顺序差异
讨论业务是否依赖顺序
1
2
3
4

10. 小检查表 ​

  • 能解释容器是否拥有元素。
  • 能说出 vector 扩容为什么会导致失效。
  • 能按访问模式选择 map 或 unordered_map。
  • 能避免把 list 当作默认性能优化。
  • 能设计不暴露容器内部地址的接口。

11. 和相邻模块的关系 ​

本篇承接内存管理模块的所有权思想,把它放入标准容器。下一篇会把容器暴露出来的遍历边界抽象成 iterator、range 和 view。

text
03-memory-management
  所有权和生命周期
  |
  v
05-01 containers
  容器拥有对象
  |
  v
05-02 iterators/ranges
  暴露遍历边界
1
2
3
4
5
6
7
8
9
10

这样划边界的好处是:学到这里时你知道自己在学哪一层。

同一个现象可能会出现在多个模块里,但每个模块关心的问题不同。

12. 本篇总结 ​

容器选择的核心不是 API 喜好,而是所有权、布局、访问模式、失效规则和诊断能力。

text
ownership + layout + access pattern + invalidation -> container choice
1

下一篇进入迭代器、Range 和 View。

13. 深入追问 ​

如果你想把这篇真正吃透,不要停在“我看懂了”。

继续追问下面这些问题:

text
为什么 vector 的连续存储影响 CPU cache?
erase 后哪些迭代器失效?
unordered_map rehash 会影响什么?
稳定引用是否一定需要节点容器?
容器接口是否泄漏了内部存储假设?
1
2
3
4
5

这些问题会把你从 API 使用者推进到工程设计者。

14. 术语辨析 ​

很多 C++ 学习困难并不是来自机制本身,而是来自几个相邻词混在一起。

学习本文主题时,至少要把下面几组词分开:

text
接口 interface
  你承诺给调用者什么

实现 implementation
  你内部怎样完成承诺

所有权 ownership
  谁负责资源生命周期

观察权 access
  谁能读取或临时引用资源

有效性 validity
  某个指针、引用、迭代器、句柄或连接此刻还能不能用

稳定性 stability
  一次修改后,外部持有的观察对象是否仍然指向同一实体

复杂度 complexity
  算法规模成本

常数项 constant factor
  同样复杂度下的真实机器成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这几组词的区别会直接影响工程判断。

例如“我能访问这个对象”不等于“我拥有这个对象”;“这个操作平均 O(1)”不等于“生产输入下没有性能尖刺”;“这个句柄数值还在”不等于“它仍然代表原来的资源”。

写代码时,可以把这组问题贴在脑子旁边:

text
我是否把所有权和观察权混了?
我是否把平均复杂度当成最坏保证?
我是否把当前有效当成修改后仍稳定?
我是否把接口承诺和内部实现细节暴露给调用者?
1
2
3
4

15. 项目化练习路线 ​

单个 API 练习只能帮助你熟悉语法。

要真正掌握本文主题,最好做一个小项目,把多个边界串起来。

text
第 1 步:写最小可运行版本
  只要求正确,不急着抽象

第 2 步:记录输入输出
  哪些数据进入,哪些产物输出

第 3 步:加入错误路径
  空输入、非法输入、资源不足、取消、重复操作

第 4 步:加入测量
  时间、分配次数、系统调用次数、连接数或内存峰值

第 5 步:替换一种实现
  换容器、换算法、换 I/O 模型、换同步方式

第 6 步:复盘取舍
  为什么新实现更好,代价是什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

项目练习的关键是保留每一步证据。

不要只留下最终版本。

每一次修改都回答:

text
我改变了什么?
我预期什么会变好?
我用什么指标验证?
有没有新的边界风险?
如果失败,是否能回到上一版?
1
2
3
4
5

16. 阅读源码时看什么 ​

当你读标准库实现、系统库封装或项目代码时,不要一上来钻进每一行。

先看结构:

text
类型定义
  这个类型代表资源、视图、策略还是算法?

构造和析构
  它是否获得或释放资源?

复制和移动
  它能否复制?移动后源对象处于什么状态?

核心操作
  哪些操作改变状态?哪些只是观察?

错误处理
  失败通过返回值、异常、错误码还是断言表达?

性能边界
  是否分配内存?是否系统调用?是否阻塞?是否遍历全部元素?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

读源码时能回答这些问题,比把实现细节背下来更重要。

17. 复盘模板 ​

每次你在项目中用到本文主题,都可以按下面模板记录一次。

text
场景
  我为什么需要这个机制?

输入
  数据、对象、文件描述符、连接、任务或配置来自哪里?

选择
  我为什么选这个容器、算法、同步原语、系统调用或网络模型?

风险
  迭代器失效、生命周期、阻塞、竞态、内存、异常、平台差异在哪里?

验证
  我怎样证明这个选择在当前项目中成立?

后续
  如果规模扩大或平台变化,哪一部分最可能先坏?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这份复盘不是形式主义。

它能帮你把一次具体实践沉淀成可迁移的判断力。

最后更新于:

Pager
上一篇1. STL 与泛型编程路线 / STL And Generic Programming Roadmap
下一篇3. 迭代器、Range 与 View 模型 / Iterator, Range, And View Model

持续记录,持续成长

Copyright © Tidenflow