前言
本文把我们迄今为止学过的 STL 容器做一次系统性回顾,范围限定为:string、vector、deque、list、stack、queue、priority_queue。
目标不是把每个 API 都罗列完,而是从**"是什么 / 底层如何实现 / 常用接口示例 / 复杂度与迭代器失效规则 / 适用场景 / 常见坑"**这几方面,给出可直接引用到博客里的、便于工程决策的总结与对比。
主要内容概览
-
各容器简介(含底层实现直觉)
-
每个容器的关键接口、复杂度与迭代器失效规则(含示例)
-
容器间重要对比(性能、内存、语义)与选型建议
-
常见优化建议与工程注意点
-
总结(一句话回顾 + 决策要点)
各容器逐项详解
1. std::string
-
是什么 / 用途
专门的字符序列容器(basic_string<char>),用于文本存储与操作,通常替代 C 风格字符串。
-
底层实现(直觉)
类似 vector<char>:内部维护连续缓冲区 + size + capacity。很多实现有 小字符串优化(SSO),短字符串直接驻留在对象内部避免分配。
-
常用接口(示例)
cppstd::string s = "hello"; s += " world"; s.append("!"); auto sub = s.substr(1,3); const char* c = s.c_str();
-
复杂度
operator[], at():O(1)
-
append/insert/erase:通常 O(n)(可能触发重新分配)
-
find:O(n)
-
-
迭代器/引用失效
-
发生重新分配(容量不足扩容)时,迭代器/指针/引用全部失效。
-
at() 提供边界检查;operator[] 不检查。
-
-
内存特性
- 连续内存,缓存友好;SSO 可减少小字符串分配。
-
适用场景
- 文本处理、拼接、格式化输出等。
-
常见坑 / 优化
- 频繁拼接时使用 reserve() 或 ostringstream;c_str() 在修改后可能失效。
2. std::vector<T>
-
是什么 / 用途
最常用的动态数组容器,适合随机访问与高效遍历。
-
底层实现(直觉)
一段连续内存,维护 start/finish/end_of_storage(或 data,size,capacity)。
-
常用接口(示例)
cppstd::vector<int> v; v.reserve(100); v.push_back(1); v.emplace_back(2); v.insert(v.begin()+1, 3); v.erase(v.begin()+2);
-
复杂度
-
随机访问:O(1)
-
push_back:均摊 O(1),单次扩容 O(n)
-
中间 insert/erase:O(n)
-
-
迭代器/引用失效
-
扩容(realloc):所有迭代器/指针/引用失效。
-
insert/erase 在中间:invalidate 从变动位置到末尾的迭代器(引用/指针对被移动元素可能失效)。
-
push_back 若不触发扩容,一般不会失效。
-
-
内存特性
- 最省内存、缓存局部性最好,遍历最快。
-
适用场景
- 随机访问、批量处理、排序、频繁遍历的主容器。
-
常见坑 / 优化
- 尽量 reserve 已知大小;函数参数用 const&;使用 move / emplace 减少拷贝。
3. std::deque<T>
-
是什么 / 用途
双端队列:既支持两端高效插入/删除,又支持随机访问。
-
底层实现(直觉)
分段缓冲(多个固定大小 block)+ 中央索引表(map of blocks),逻辑上连续,物理上分段。
-
常用接口(示例)
cppstd::deque<int> dq; dq.push_front(1); dq.push_back(2); dq.pop_front(); dq.pop_back(); dq[2]; // 随机访问
-
复杂度
-
push_front / push_back:O(1)(均摊)
-
随机访问:O(1)(比 vector 稍慢常数)
-
中间插入/删除:O(n)
-
-
迭代器/引用失效
-
两端插入/删除 :实现相关,但保守地认为迭代器 可能 失效;引用/指针通常保持有效(对已存在元素,非被删元素)。
-
中间插入/删除:可能会使迭代器与引用失效(会移动元素)。
-
-
内存特性
- 每个 block 分配;相比 vector 有额外开销;缓存局部性介于 vector 和 list 之间。
-
适用场景
- 需要在两端频繁操作并且还要随机访问(如滑动窗口、双端缓冲)。
-
常见坑 / 优化
- 不要依赖迭代器在两端操作后仍然有效;中间大量插入应考虑其它数据结构。
4. std::list<T>(双向链表)
-
是什么 / 用途
节点式双向链表,适合在任意位置 O(1) 插入/删除(已知位置)。
-
底层实现(直觉)
每个节点包含数据 + 前驱/后继指针;每节点单独分配(allocator)。
-
常用接口(示例)
cppstd::list<int> L = {1,2,3}; auto it = std::next(L.begin(), 1); L.insert(it, 42); // O(1) L.erase(it); // O(1) L.splice(other, it_begin, it_end); // O(1) 节点移动
-
复杂度
-
插入/删除(已知迭代器位置):O(1)
-
随机访问:O(n)
-
-
迭代器/引用失效
- 插入不使其他节点迭代器失效;erase(it) 只使被删迭代器失效。迭代器稳定性最好(除被删元素)。
-
内存特性
- 每节点开销大(指针 + 分配器元信息);缓存友好性差。
-
适用场景
- 频繁中间插删,且需要迭代器/引用稳定(比如需要 splice 的场景)。
-
常见坑 / 优化
- 不要为了避免对象移动就盲用 list;很多情况下 vector + move/index 更快。避免在性能敏感场景随意使用 list。
5. std::stack<T>(适配器)
-
是什么 / 用途
容器适配器,提供 LIFO(后进先出)接口:push/pop/top。并非独立容器。
-
底层实现
默认用 deque<T>,也可以指定 vector<T> 或 list<T> 作为底层(模板参数)。
-
常用接口(示例)
cppstd::stack<int> st; st.push(1); st.push(2); auto t = st.top(); st.pop();
-
复杂度
- push/pop/top:O(1)(底层容器决定细节)。
-
迭代器/引用
- stack 不提供迭代器;top() 返回的引用在 pop() 后失效。
-
适用场景
- 括号匹配、DFS(显式非递归)、撤销栈等。
-
常见坑
- stack 隐藏了底层容器,若需要遍历或随机访问直接使用底层容器(deque/vector)。
6. std::queue<T>(适配器)
-
是什么 / 用途
容器适配器,提供 FIFO(先进先出)接口:push/pop/front/back。
-
底层实现
默认 deque<T>,也可以用 list<T>。不能用 vector(无 pop_front)。
-
常用接口(示例)
cppstd::queue<int> q; q.push(1); q.push(2); auto f = q.front(); q.pop();
-
复杂度
- 基本操作 O(1)(由底层容器决定)。
-
迭代器/引用
- queue 不提供迭代器;front()/back() 的引用在 pop() 后失效。
-
适用场景
- BFS、任务队列、消息转发等。
-
常见坑
- 无 clear()(可通过 swap 空队列或循环 pop 实现);并发场景需加锁或用并发队列。
7. std::priority_queue<T>(优先队列)
-
是什么 / 用途
按优先级出队的容器适配器(heap 实现),top() 返回优先级最高元素。
-
底层实现
默认底层是 std::vector<T>,通过二叉堆算法实现(make_heap/push_heap/pop_heap)。
-
常用接口(示例)
cppstd::priority_queue<int> pq; // max-heap pq.push(3); pq.push(1); pq.pop(); // min-heap: std::priority_queue<int, std::vector<int>, std::greater<int>> minpq;
-
复杂度
-
top():O(1)
-
push/pop:O(log n)
-
从范围构造(make_heap):O(n)
-
-
迭代器/引用
- priority_queue 不提供迭代器;底层 vector 的 reallocate 会使指针/引用失效。
-
适用场景
Dijkstra、事件驱动仿真、top-k 问题、调度器等。
-
常见坑
堆不是稳定的;若需要稳定性需要额外的 tiebreaker(如时间戳)。
容器比较(关键维度对照)
容器 | 内存布局 | 随机访问 | 头/尾插删 | 中间插删 | 迭代器稳定性 | 缓存局部性 | 典型适用 |
---|---|---|---|---|---|---|---|
string | 连续 | O(1) | 末尾快 | O(n) | 扩容会失效 | 很好 | 文本处理 |
vector | 连续 | O(1) | 末尾快 | O(n) | 扩容会失效 | 最好 | 大多数序列需求(默认) |
deque | 分段连续 | O(1) | 两端快 | O(n) | 两端操作迭代器可能失效 | 较好 | 两端操作 + 随机访问 |
list | 节点 | 不支持 | O(1)(已知位) | O(1)(已知位) | 插入不失效,erase 仅失效被删 | 差 | 频繁中间插删、splice |
stack (adapter) | 由底层决定 | 依赖底层 | 依赖底层 | --- | 不提供迭代器 | --- | LIFO 语义 |
queue (adapter) | 由底层决定 | 依赖底层 | 依赖底层 | --- | 不提供迭代器 | --- | FIFO 语义 |
priority_queue | heap on vector | 不直接支持 | push: O(log n) | --- | 不提供迭代器 | 受 vector 影响 | 优先级调度、top-k |
选型与工程化建议(快速决策要点)
-
默认首选 vector:能满足多数场景且性能最好。
-
两端操作频繁且仍需随机访问 → deque。
-
需要在中间大量插删且需要迭代器稳定 → list(先确认是否真需要)。
-
需要 FIFO / LIFO / priority 语义 → queue / stack / priority_queue(适配器,底层通常 deque/vector)。
-
频繁扩容场景:尽早 reserve();对 priority_queue 在已知元素构造时使用区间构造(O(n))。
-
避免长期持有迭代器/引用:任何修改后应谨慎重新获取。
-
避免误用 list 来"避免移动对象":常见误区,vector 的移动/交换往往更快且内存更经济。
-
并发使用:STL 容器非线程安全,生产-消费场景加锁或使用专用并发结构。
常见优化技巧(实践可复制)
-
对 vector/string:reserve(n) 避免扩容;用 emplace_back 原地构造;传参使用 const T&,返回使用移动语义。
-
对 unordered_*:若能估算元素数用 reserve() 预留桶,减少 rehash。
-
对 priority_queue:若已知全部数据,优先用区间构造(make_heap)而不是逐个 push。
-
对 list:用 splice 在容器间 O(1) 移动节点,避免元素拷贝。
-
总是测 perf:关键路径用基准测试(profiling)胜过直觉。
总结(结论性回顾)
-
string / vector / deque:属于连续或分段连续存储 ,以缓存友好与随机访问为主;其中 vector 是通用默认容器,deque 平衡了两端操作;string 专用于字符。
-
list:节点式容器,优势在 中间插删与迭代器稳定性,代价是内存开销与差的缓存局部性。
-
stack / queue / priority_queue:语义清晰的容器适配器,它们隐藏底层细节以暴露特定接口:stack/queue操作 O(1)(底层决定),priority_queue 基于堆(push/pop O(log n))。
-
工程实践要点:默认用 vector,明确定义需求(随机访问 vs. 中间插删 vs. 两端操作 vs. 优先级)再选型;始终关注迭代器失效规则与内存/缓存影响。