C++ 之 STL 容器详解(包含十种常用容器)
STL(Standard Template Library,标准模板库)是 C++ 标准库的核心组成部分,其中容器(Containers)作为数据存储的基本单元,为开发者提供了高效、灵活的数据结构解决方案。STL 容器分为序列式容器、关联式容器和无序关联式容器三大类,每种容器都有其独特的底层实现和适用场景。本文将详细讲解十种常用 STL 容器的特性、用法及最佳实践。
一、STL 容器概述与分类
STL 容器是模板类的集合,用于存储和管理对象集合,其核心价值在于:
- 封装了数据结构的实现细节,开发者无需重复造轮子
- 提供统一的接口(如begin()/end()迭代器、size()/empty()等),降低学习成本
- 与 STL 算法(如sort/find)无缝配合,提升开发效率
容器分类
类别 | 核心特性 | 常用容器示例 |
序列式容器 | 元素按插入顺序存储,无自动排序 | vector、list、deque、array、string |
关联式容器 | 元素按键(Key)自动排序,基于红黑树实现 | set、multiset、map、multimap |
无序关联式容器 | 元素无序存储,基于哈希表实现 | unordered_set、unordered_map、unordered_multiset、unordered_multimap |
二、十种常用容器详解
1. vector:动态数组(序列式容器)
底层实现
基于动态数组实现,内存连续分配,当容量不足时自动扩容(通常扩容为原容量的 1.5 倍或 2 倍)。
核心特性
- 支持随机访问(通过下标[]或at()访问,时间复杂度 O (1))
- 尾部插入 / 删除效率高(O (1)),中间 / 头部插入 / 删除效率低(需移动元素,O (n))
- 内存连续,缓存友好,但扩容时可能导致迭代器失效
常用操作
#include <vector>vector<int> vec;// 插入元素vec.push_back(10); // 尾部插入vec.insert(vec.begin(), 5); // 头部插入(效率低)// 访问元素int first = vec[0]; // 随机访问,无越界检查int second = vec.at(1); // 随机访问,有越界检查(抛异常)// 删除元素vec.pop_back(); // 尾部删除vec.erase(vec.begin() + 1); // 删除指定位置元素// 其他操作vec.size(); // 元素个数vec.empty(); // 是否为空vec.reserve(100); // 预留容量,避免频繁扩容vec.clear(); // 清空元素(不释放内存)
适用场景
- 需要频繁随机访问元素
- 尾部插入 / 删除操作较多
- 已知或可预估元素数量(通过reserve优化性能)
- 替代 C 风格数组,避免手动管理内存
2. list:双向链表(序列式容器)
底层实现
基于双向链表实现,每个节点包含数据域和两个指针(前驱、后继),内存不连续。
核心特性
- 不支持随机访问(访问第 n 个元素需遍历,O (n))
- 任意位置插入 / 删除效率高(只需修改指针,O (1))
- 内存不连续,缓存友好性差,但插入 / 删除不会导致其他元素的迭代器失效
常用操作
#include <list>list<int> lst;// 插入元素lst.push_back(20); // 尾部插入lst.push_front(10); // 头部插入lst.insert(++lst.begin(), 15); // 中间插入// 访问元素(无随机访问,需遍历)for (auto it = lst.begin(); it != lst.end(); ++it) { cout << *it << " ";}// 删除元素lst.pop_back(); // 尾部删除lst.pop_front(); // 头部删除lst.erase(lst.begin()); // 删除指定位置元素// 特有操作lst.sort(); // 链表排序(O(n log n))lst.unique(); // 移除连续重复元素lst.reverse(); // 反转链表
适用场景
- 需要频繁在任意位置插入 / 删除元素
- 无需随机访问元素
- 元素数量不确定,且频繁动态变化
3. deque:双端队列(序列式容器)
底层实现
基于分段连续内存(多个固定大小的数组 + 中控器)实现,支持高效的头尾操作。
核心特性
- 支持随机访问(O (1)),但效率略低于vector
- 头部和尾部插入 / 删除效率高(O (1)),中间插入 / 删除效率低(O (n))
- 无固定扩容策略,内存分配更灵活,避免vector的大量拷贝
常用操作
#include <deque>deque<int> dq;// 插入元素dq.push_back(30); // 尾部插入dq.push_front(20); // 头部插入dq.insert(dq.begin() + 1, 25); // 中间插入// 访问元素int front = dq[0]; // 头部元素int back = dq.back(); // 尾部元素// 删除元素dq.pop_back(); // 尾部删除dq.pop_front(); // 头部删除
适用场景
- 需要高效的头尾插入 / 删除操作
- 需要随机访问,但元素数量波动较大(避免vector的扩容开销)
- 作为队列(FIFO)或双端队列的首选容器
4. array:静态数组(序列式容器)
底层实现
基于固定大小的静态数组实现,编译期确定大小,内存连续。
核心特性
- 支持随机访问(O(1))
- 大小固定,不可动态扩容 / 缩容
- 栈上分配内存(小对象),效率高于vector,但灵活性低
常用操作
#include <array>array<int, 5> arr = {1, 2, 3, 4, 5}; // 大小固定为5// 访问元素int val = arr[2]; // 随机访问int first = arr.front(); // 第一个元素int last = arr.back(); // 最后一个元素// 迭代器遍历for (auto it = arr.begin(); it != arr.end(); ++it) { cout << *it << " ";}// 大小相关arr.size(); // 固定为5(编译期确定)arr.max_size(); // 同size()
适用场景
- 元素数量已知且固定不变
- 需要最高效的随机访问和内存性能
- 替代 C 风格静态数组(提供 STL 容器接口,更安全)
5. string:字符串容器(序列式容器)
底层实现
本质是vector<char>的特化,基于动态字符数组实现,以'\0'结尾兼容 C 风格字符串。
核心特性
- 支持随机访问字符(O (1))
- 尾部拼接效率高,支持字符串常量直接拼接
- 提供丰富的字符串操作函数(查找、替换、截取等)
常用操作
#include <string>string str = "hello";// 拼接操作str += " world"; // 尾部拼接str.append("!"); // 追加字符串// 访问字符char c = str[4]; // 'o'char last = str.back(); // '!'// 查找与替换size_t pos = str.find("world"); // 查找子串位置string sub = str.substr(6, 5); // 截取子串"world"// 修改操作str.replace(0, 5, "hi"); // 替换为"hi world!"str.erase(2, 1); // 删除位置2的字符
适用场景
- 字符串存储与处理
- 需要频繁拼接、查找、截取字符串
- 替代 C 风格字符串(char*),自动管理内存
6. set:有序集合(关联式容器)
底层实现
基于红黑树(自平衡二叉搜索树)实现,元素按键自动排序(默认升序)。
核心特性
- 元素唯一且有序(默认less<T>排序)
- 插入、删除、查找操作效率高(O (log n))
- 不支持随机访问,迭代器为双向迭代器
常用操作
#include <set>set<int> s;// 插入元素(自动去重+排序)s.insert(30);s.insert(10);s.insert(20);s.insert(20); // 重复元素,插入失败// 查找元素auto it = s.find(20); // 查找成功返回迭代器if (it != s.end()) { cout << "找到元素:" << *it << endl;}// 删除元素s.erase(10); // 按值删除s.erase(s.begin()); // 按迭代器删除// 范围操作auto range = s.lower_bound(20); // 首个>=20的元素
适用场景
- 需要自动排序且无重复元素的集合
- 频繁查找、插入、删除元素(中等规模数据)
- 需按范围查询(如lower_bound/upper_bound)
7. multiset:有序多重集合(关联式容器)
底层实现
基于红黑树实现,与set的唯一区别是允许重复元素。
核心特性
- 元素有序但可重复,排序规则与set一致
- 插入、删除、查找效率为 O (log n)
- 支持统计元素出现次数(count()方法)
常用操作
#include <set>multiset<int> ms;// 插入元素(允许重复)ms.insert(20);ms.insert(10);ms.insert(20); // 重复元素可插入// 统计元素出现次数int cnt = ms.count(20); // 返回2// 删除元素(删除所有值为20的元素)ms.erase(20);// 范围查找(获取所有值为20的元素)auto pair = ms.equal_range(20);for (auto it = pair.first; it != pair.second; ++it) { cout << *it << " ";}
适用场景
- 需要自动排序且允许重复元素的集合
- 需统计元素出现次数或处理重复数据
- 替代set处理有重复值的场景
8. map:有序键值对容器(关联式容器)
底层实现
基于红黑树实现,存储key-value键值对,按key 自动排序,key 唯一。
核心特性
- 键值对集合,key 唯一且有序,支持通过 key 快速访问 value
- 插入、删除、查找效率为 O (log n)
- 迭代器遍历顺序为 key 的排序顺序
常用操作
#include <map>map<string, int> mp;// 插入键值对mp["Alice"] = 90; // 下标插入mp.insert({"Bob", 85}); // insert方法插入mp.emplace("Charlie", 95); // 原位构造(更高效)// 查找元素auto it = mp.find("Bob"); // 按key查找if (it != mp.end()) { cout << "Bob的分数:" << it->second << endl;}// 删除元素mp.erase("Alice"); // 按key删除// 遍历键值对for (const auto& pair : mp) { cout << pair.first << ":" << pair.second << endl;}
适用场景
- 需要键值对映射且 key 有序的场景
- 频繁通过 key 查找 value(如字典、索引表)
- 需要按 key 排序的键值对集合
9. multimap:有序多重键值对容器(关联式容器)
底层实现
基于红黑树实现,与map的区别是允许key 重复。
核心特性
- 键值对集合,key 可重复且有序
- 插入、删除、查找效率为 O (log n)
- 支持按 key 获取所有关联的 value(equal_range)
常用操作
#include <map>multimap<string, int> mmp;// 插入键值对(允许重复key)mmp.insert({"Alice", 90});mmp.insert({"Alice", 95}); // 重复keymmp.insert({"Bob", 85});// 查找key对应的所有valueauto range = mmp.equal_range("Alice");for (auto it = range.first; it != range.second; ++it) { cout << it->first << ":" << it->second << endl;}// 统计key出现次数int cnt = mmp.count("Alice"); // 返回2
适用场景
- 一个 key 对应多个 value 的场景(如一对多映射)
- 需要按 key 排序的多重键值对集合
- 如索引表(一个关键词对应多个文档)、分类统计等
10. unordered_map:无序键值对容器(无序关联式容器)
底层实现
基于哈希表实现,通过哈希函数将 key 映射到桶(Bucket),存储key-value键值对。
核心特性
- 键值对集合,key 唯一且无序
- 平均插入、删除、查找效率为 O (1),最坏情况 O (n)(哈希冲突严重时)
- 内存占用较高,迭代器为前向迭代器
常用操作
#include <unordered_map>unordered_map<string, int> ump;// 插入键值对ump["Apple"] = 5;ump.insert({"Banana", 3});// 查找元素(平均O(1))auto it = ump.find("Apple");if (it != ump.end()) { cout << "Apple数量:" << it->second << endl;}// 遍历(无序)for (const auto& pair : ump) { cout << pair.first << ":" << pair.second << endl;}// 哈希表参数调整ump.reserve(100); // 预留桶数量,减少扩容ump.max_load_factor(0.7f); // 设置负载因子(元素数/桶数)
适用场景
- 需要高效查找(O (1))且不关心顺序的键值对场景
- 高频查找操作(如缓存、字典)
- 数据量较大,且哈希函数设计良好(减少冲突)
三、容器对比与选择指南
核心特性对比表
容器类型 | 底层结构 | 有序性 | 重复性 | 随机访问 | 插入 / 删除效率(平均) | 查找效率(平均) |
vector | 动态数组 | 插入序 | 允许 | 支持 | 尾部 O (1),中间 O (n) | O(n) |
list | 双向链表 | 插入序 | 允许 | 不支持 | 任意位置 O (1) | O(n) |
deque | 分段数组 | 插入序 | 允许 | 支持 | 头尾 O (1),中间 O (n) | O(n) |
array | 静态数组 | 插入序 | 允许 | 支持 | 不支持动态修改 | O(n) |
string | 动态字符数组 | 插入序 | 允许 | 支持 | 尾部 O (1),中间 O (n) | O(n) |
set | 红黑树 | 排序序 | 不允许 | 不支持 | O(log n) | O(log n) |
multiset | 红黑树 | 排序序 | 允许 | 不支持 | O(log n) | O(log n) |
map | 红黑树 | 排序序 | key 不允许 | 不支持 | O(log n) | O(log n) |
multimap | 红黑树 | 排序序 | key 允许 | 不支持 | O(log n) | O(log n) |
unordered_map | 哈希表 | 无序 | key 不允许 | 不支持 | O(1) | O(1) |
容器选择决策树
- 是否需要键值对存储?
- 是 → 进入关联式 / 无序关联式容器选择
- 否 → 进入序列式容器选择
- 序列式容器选择:
- 需要随机访问 → vector(动态)或array(静态)
- 频繁任意位置插入 / 删除 → list
- 频繁头尾操作 → deque
- 字符串处理 → string
- 关联式 / 无序关联式容器选择:
- 需要有序存储 → 红黑树系列(set/map/multiset/multimap)
- 需要最高查找效率 → 哈希表系列(unordered_set/unordered_map)
- 允许重复元素 → multiset/multimap
- 唯一元素 → set/map/unordered_set/unordered_map
四、容器使用最佳实践
- 避免不必要的拷贝:对大容器使用引用传递(const&),C++11 后优先使用移动语义(std::move)。
- 预分配内存:vector和string使用reserve()预分配容量,unordered_map使用reserve()设置桶数量,减少扩容开销。
- 选择合适的迭代器操作:随机访问容器优先使用[]或at(),避免迭代器遍历访问;链表容器避免随机访问。
- 注意迭代器失效:
- vector/string:插入可能导致部分或全部迭代器失效,删除导致当前及之后迭代器失效。
- list:插入不影响迭代器,删除仅导致当前迭代器失效。
- 关联式容器:插入 / 删除不影响其他元素的迭代器。
- 哈希容器优化:为自定义类型设计良好的哈希函数,避免过多哈希冲突;合理设置负载因子(通常 0.7~0.8)。
- 优先使用标准容器:除非有特殊需求,否则避免重复实现类似功能的自定义容器,标准容器经过充分优化和测试。
总结
STL 容器为 C++ 开发者提供了丰富的数据结构选择,每种容器都有其独特的底层实现和适用场景。掌握十种常用容器的特性,关键在于理解其底层数据结构对性能的影响(如随机访问、插入 / 删除效率),并根据具体场景的需求(有序性、重复性、操作类型)选择最合适的容器。
实际开发中,需结合数据规模、操作频率和性能要求综合决策,同时遵循最佳实践以避免常见陷阱(如迭代器失效、不必要的拷贝)。通过合理使用 STL 容器,可显著提升代码效率和可维护性。