什么是容器?
C++ 容器是 C++ 标准模板库(STL)提供的、封装好的通用数据结构模板类,它以类型安全的方式存储和管理一组同类型的元素,通过 RAII 机制自动管理元素的内存生命周期,是现代 C++ 开发中替代原生数组、手动实现数据结构的标准方案。
序列式容器
序列式容器是按元素的「插入顺序」存储和组织的容器,元素的位置由插入时机和位置决定,不会自动按元素值排序,是最基础、最常用的容器类别。
1. std::vector(动态数组)
连续内存的动态数组:在堆上分配一块连续的内存空间,存储元素,当空间不足时自动扩容(通常是 1.5 倍或 2 倍增长)。
核心特点
- 随机访问 O (1) :通过下标
[]或at()直接访问任意位置元素,速度极快。 - 尾部增删 O (1) :
push_back()/pop_back()在尾部操作,平均复杂度 O (1)(扩容时除外)。 - 中间 / 头部增删 O (n) :在中间或头部插入 / 删除元素,需要移动后面的所有元素,性能较差。
- 缓存友好:连续内存设计让 CPU 缓存命中率极高,遍历性能远超其他容器。
- RAII 自动管理:离开作用域自动析构,释放所有元素内存,哪怕抛异常也不泄漏。
关键接口
| 接口 | 作用 | 注意事项 |
|---|---|---|
push_back(val) | 尾部插入元素 | 会调用拷贝构造,推荐用 emplace_back |
emplace_back(args...) | 尾部直接构造元素 | 零拷贝,直接在容器内存中构造,性能更高 |
pop_back() | 尾部删除元素 | 不返回被删元素,要取先 back() |
reserve(n) | 预分配至少 n 个元素的空间 | 提前知道数据量时用,避免频繁扩容 |
resize(n, val) | 调整容器大小为 n | 扩容时用 val 填充新元素,缩容时析构多余元素 |
[] / at() | 随机访问元素 | [] 不做边界检查(越界未定义),at() 越界抛异常 |
适用场景:
- 90% 的通用存储场景:不知道用什么容器时,默认选
vector。 - 需要随机访问:通过下标快速访问元素。
- 仅尾部频繁增删:不在中间 / 头部频繁操作。
- 替代原生数组:彻底告别原生数组的越界、手动管理内存问题。
2. std::deque(双端队列)
分段连续内存(中控器 + 缓冲区) :不是一整块连续内存,而是由多块 “缓冲区” 组成,通过一个 “中控器”(指针数组)管理这些缓冲区。
核心特点
- 头尾增删 O (1) :
push_front()/push_back()/pop_front()/pop_back()都极快,不需要移动元素。 - 随机访问 O (1) :支持下标访问,但略慢于
vector(需要通过中控器计算位置)。 - 中间增删 O (n) :和
vector一样,中间插入 / 删除需要移动元素。 - 缓存不如 vector 友好:分段内存设计导致 CPU 缓存命中率略低。
适用场景
- 需要头尾双向频繁增删:比如滑动窗口、任务队列、消息队列。
- 需要随机访问,但头部也需要增删:
vector不支持push_front(),此时用deque。 - 容器适配器的底层默认:
std::stack和std::queue的默认底层容器都是deque。
3. std::list(双向循环链表)
双向循环链表:每个元素是一个节点,包含数据、前驱指针、后继指针,节点之间通过指针连接,内存不连续。
核心特点
- 任意位置增删 O (1) :在任意位置插入 / 删除元素,只需要修改前后节点的指针,不需要移动元素。
- 不支持随机访问:没有下标
[],只能通过迭代器顺序遍历(从前往后或从后往前)。 - 遍历性能差:内存不连续,CPU 缓存命中率极低,遍历速度远慢于
vector。 - 内存开销大:每个节点额外存储两个指针(前驱 + 后继),内存占用比
vector高。
关键接口
| 接口 | 作用 | 优势 |
|---|---|---|
push_front()/push_back() | 头尾插入 | O(1) |
insert(pos, val) | 在迭代器 pos 前插入 | O (1)(只需修改指针) |
erase(pos) | 删除迭代器 pos 处的元素 | O (1)(只需修改指针) |
splice(pos, other) | 将 other 链表的所有元素移动到 pos 前 | O (1) 零成本,不拷贝元素 |
适用场景
- 频繁在容器中间插入 / 删除元素:比如编辑器的文本插入、链表排序。
- 不需要随机访问:仅通过迭代器顺序遍历。
- 需要高效的链表拼接:
splice()可以零成本拼接两个链表。
4. std::forward_list(单向链表,C++11 引入)
单向链表:每个节点只有数据和后继指针,没有前驱指针,只能单向遍历。
核心特点
- 比
list更省内存:少存一个前驱指针,内存开销更小。 - 只能单向遍历:没有反向迭代器,不能从后往前遍历。
- 不支持
push_back()/pop_back():只能在头部或指定位置操作。 - 使用场景极少:现代 C++ 开发中几乎不用,除非是极致内存优化的场景。
5. std::array(固定大小数组,C++11 引入)
封装的原生固定数组:在栈上(或对象内部)分配固定大小的连续内存,大小在编译期确定,不能改变。
核心特点
- 类型安全:替代原生数组,不会隐式退化为指针,支持
size()/empty()等接口。 - 性能和原生数组一样:没有任何额外开销,编译期确定大小。
- 大小固定:不能扩容或缩容,编译器必须知道大小。
适用场景
- 大小固定且已知:比如存储一周 7 天、一年 12 个月。
- 替代原生固定数组:既保持原生数组的性能,又有类型安全和统一接口。
有序关联容器
有序关联容器是基于「红黑树(Red-Black Tree)」实现的、自动按 key 排序的关联式容器,它以 “键值对” 或 “单一键” 的形式存储元素,所有元素会按 key 的大小自动升序排列,查找、插入、删除的时间复杂度稳定为 O(logn) 。
1. std::map(有序键值对字典,最常用)
std::map 是存储「键值对(std::pair<const Key, Value>)」的有序容器,key 必须唯一,所有元素按 key 自动升序排列。
核心特点
- key 唯一:同一个 key 只能存在一个元素,插入重复 key 会失败(或覆盖,取决于插入方式)。
- 自动按 key 排序:默认用
std::less<Key>比较,也可以自定义比较函数。 - 稳定 O (logn) 复杂度:查找、插入、删除都是 O (logn)。
- 支持范围查找:可以高效查找 key 在某个区间内的所有元素。
- 平均查找速度略慢于
std::unordered_map:红黑树的 O (logn) 比哈希表的平均 O (1) 略慢。
关键接口
| 接口 | 作用 | 注意事项 |
|---|---|---|
insert({key, val}) | 插入键值对 | key 已存在时插入失败,返回 pair<iterator, bool> |
emplace(key, val) | 直接构造键值对插入 | 零拷贝,性能更高 |
at(key) | 访问 key 对应的 value | key 不存在时抛 std::out_of_range 异常,更安全 |
find(key) | 查找 key 对应的迭代器 | 找到返回迭代器,没找到返回 end() |
count(key) | 统计 key 出现的次数 | 对于 map 只能是 0 或 1 |
erase(key) / erase(iter) | 删除元素 | 删除 key 或迭代器指向的元素,O (logn) |
lower_bound(key) | 返回第一个 ≥ key 的迭代器 | 范围查找的核心接口 |
upper_bound(key) | 返回第一个 > key 的迭代器 | 范围查找的核心接口 |
equal_range(key) | 返回 pair<lower_bound, upper_bound> | 一次性获取 key 对应的范围 |
适用场景
- 需要有序的键值对映射:比如按用户 ID 升序存储用户信息、按时间顺序存储日志。
- 需要范围查找:比如查找 “ID 在 100 到 200 之间的所有用户”。
- 需要按 key 顺序遍历:比如按分数从低到高输出排行榜。
- 需要稳定的 O (logn) 性能:不能接受哈希表最坏情况 O (n) 的场景。
2. std::set(有序唯一键集合)
std::set 是存储「单一键(Key)」的有序容器,key 必须唯一,所有元素按 key 自动升序排列,本质上是 std::map 的 “简化版”(只有 key,没有 value)。
核心特点
- key 唯一:同一个 key 只能存在一个元素。
- 自动按 key 排序:默认升序,可自定义比较函数。
- 稳定 O (logn) 复杂度:查找、插入、删除都是 O (logn)。
- 支持范围查找:可以高效查找某个区间内的所有 key。
- 同时完成去重 + 排序:插入元素时自动去重,且自动排序。
关键接口
和 std::map 类似,但没有 operator[]/at(),因为只有 key 没有 value:
| 接口 | 作用 |
|---|---|
insert(key) / emplace(key) | 插入 key |
find(key) | 查找 key |
count(key) | 统计 key 出现次数(0 或 1) |
erase(key) / erase(iter) | 删除 key |
lower_bound(key) / upper_bound(key) | 范围查找 |
适用场景
- 需要同时去重 + 排序:比如存储有序的不重复整数集合、去重后的有序单词列表。
- 需要快速判断元素是否存在:比如黑名单、白名单,且需要按顺序遍历。
- 需要范围查找:比如查找 “分数在 80 到 90 之间的所有学生”。
3. std::multimap(有序可重复键值对字典)
std::multimap 是 std::map 的 “可重复版本”,允许 key 重复,其他特性和 std::map 完全一致(自动排序、O (logn) 复杂度、范围查找)。
核心特点
- 允许 key 重复:同一个 key 可以对应多个 value。
- 自动按 key 排序:相同 key 的元素会按插入顺序排列在一起。
- 稳定 O (logn) 复杂度。
- 支持范围查找:可以通过
equal_range(key)一次性获取某个 key 对应的所有 value。
关键接口
和 std::map 类似,但没有 operator[]/at() (因为 key 重复,无法确定返回哪个 value),主要用 equal_range(key) 获取重复 key 的范围。
适用场景
- 一对多的有序映射:比如按 “班级” 存储多个学生(同一个班级对应多个学生)、按 “日期” 存储多条日志。
- 需要有序的重复键值对:比如按 “分数” 存储多个学生(允许同分),且按分数排序。
4. std::multiset(有序可重复键集合)
std::multiset 是 std::set 的 “可重复版本”,允许 key 重复,其他特性和 std::set 完全一致(自动排序、O (logn) 复杂度、范围查找)。
核心特点
- 允许 key 重复:同一个 key 可以出现多次。
- 自动按 key 排序:相同 key 的元素会按插入顺序排列在一起。
- 稳定 O (logn) 复杂度。
- 支持范围查找:可以通过
equal_range(key)一次性获取某个 key 对应的所有元素。
适用场景
- 需要有序的重复元素集合:比如存储有序的分数列表(允许同分)、有序的单词频率统计。
- 需要同时排序 + 允许重复:比如按时间顺序存储所有事件(允许同一时间发生多个事件)。
无序关联容器
无序关联容器是 C++11 引入的、基于「哈希表(Hash Table)」实现的关联式容器,它以 “键值对” 或 “单一键” 的形式存储元素,元素无序,查找、插入、删除的平均时间复杂度为 O (1) ,是现代 C++ 中快速查找、去重场景的首选。
1. std::unordered_map(无序键值对字典,最常用)
std::unordered_map 是存储「键值对(std::pair<const Key, Value>)」的无序容器,key 必须唯一,通过哈希函数快速定位元素,平均 O (1) 性能。
核心特点
- key 唯一:同一个 key 只能存在一个元素。
- 平均 O (1) 极速查找 / 插入 / 删除:性能远快于
std::map。 - 元素无序:不按 key 排序,遍历顺序不确定。
- 不支持范围查找:无法高效查找 key 在某个区间内的元素。
- 最坏情况 O (n) :哈希冲突严重时性能退化。
关键接口
和 std::map 高度一致,但没有范围查找接口:
| 接口 | 作用 | 注意事项 |
|---|---|---|
insert({key, val}) | 插入键值对 | key 已存在时插入失败,返回 pair<iterator, bool> |
emplace(key, val) | 直接构造键值对插入 | 零拷贝,性能更高 |
at(key) | 访问 key 对应的 value | key 不存在时抛 std::out_of_range 异常,更安全 |
find(key) | 查找 key 对应的迭代器 | 找到返回迭代器,没找到返回 end() |
count(key) | 统计 key 出现的次数 | 只能是 0 或 1 |
erase(key) / erase(iter) | 删除元素 | O (1) 平均复杂度 |
reserve(n) | 预分配至少 n 个元素的空间 | 提前知道数据量时用,避免频繁重新哈希 |
bucket_count() | 返回当前桶的数量 | 用于调试哈希表状态 |
load_factor() | 返回当前负载因子 | 用于监控哈希表拥挤程度 |
适用场景
- 绝大多数字典 / 映射场景:不需要排序,仅需要快速查找、插入、删除,比如缓存系统、用户 ID 到信息的映射、统计计数。
- 高性能去重 + 映射:比如统计单词出现频率、IP 地址访问次数。
- 替代
std::map的首选:只要不需要有序和范围查找,优先用unordered_map,性能更高。
2. std::unordered_set(无序唯一键集合)
std::unordered_set 是存储「单一键(Key)」的无序容器,key 必须唯一,通过哈希函数快速定位元素,平均 O (1) 性能,本质上是 std::unordered_map 的 “简化版”(只有 key,没有 value)。
核心特点
- key 唯一:同一个 key 只能存在一个元素。
- 平均 O (1) 极速查找 / 插入 / 删除:性能远快于
std::set。 - 元素无序:不按 key 排序。
- 不支持范围查找。
关键接口
和 std::unordered_map 类似,但没有 operator[]/at():
| 接口 | 作用 |
|---|---|
insert(key) / emplace(key) | 插入 key |
find(key) | 查找 key |
count(key) | 统计 key 出现次数(0 或 1) |
erase(key) / erase(iter) | 删除 key |
reserve(n) | 预分配空间 |
适用场景
- 高性能去重:比如存储不重复的用户 ID、IP 地址、单词列表。
- 快速判断元素是否存在:比如黑名单、白名单、访问记录统计,不需要排序。
- 替代
std::set的首选:只要不需要有序和范围查找,优先用unordered_set。
3. std::unordered_multimap(无序可重复键值对字典)
std::unordered_multimap 是 std::unordered_map 的 “可重复版本”,允许 key 重复,其他特性完全一致(平均 O (1) 性能、无序、不支持范围查找)。
核心特点
- 允许 key 重复:同一个 key 可以对应多个 value。
- 平均 O (1) 性能。
- 元素无序。
- 不支持范围查找。
- 没有
operator[]/at():因为 key 重复,无法确定返回哪个 value。
关键接口
主要用 equal_range(key) 获取重复 key 的范围。
适用场景
- 一对多的无序映射:比如按 “班级” 存储多个学生、按 “日期” 存储多条日志,不需要排序。
- 需要高性能的重复键值对:比如按 “关键词” 存储多个搜索结果,不需要排序。
4. std::unordered_multiset(无序可重复键集合)
std::unordered_multiset 是 std::unordered_set 的 “可重复版本”,允许 key 重复,其他特性完全一致(平均 O (1) 性能、无序)。
核心特点
- 允许 key 重复。
- 平均 O (1) 性能。
- 元素无序。
适用场景
- 高性能的重复元素集合:比如存储访问日志的 IP 地址(允许重复)、统计单词出现频率(允许重复),不需要排序。
容器适配器
容器适配器是基于「适配器设计模式」,对现有序列式容器进行二次封装,屏蔽通用接口,仅暴露特定场景专用受限接口的模板类。 它本身不是独立的容器,不直接管理内存、不实现数据存储逻辑,所有核心操作都委托给底层的序列式容器,相当于给通用容器套了一层 “功能外壳”,把灵活的通用容器,转换成语义绝对明确、接口高度受限的专用数据结构。
1. std::stack(栈适配器)
std::stack 是实现 后进先出(LIFO, Last In First Out) 语义的容器适配器,所有操作都只能在 栈顶(尾部) 进行,无法访问栈内的其他元素。
底层实现
- 默认底层容器:
std::deque(双端队列) - 可选底层容器:
std::vector/std::list(只要支持back()、push_back()、pop_back()三个接口即可)
核心接口
| 接口 | 作用 | 时间复杂度 |
|---|---|---|
push(val) / emplace(args...) | 元素入栈(压入栈顶) | O (1)(底层容器决定) |
pop() | 栈顶元素出栈(无返回值) | O(1) |
top() | 获取栈顶元素的引用 | O(1) |
empty() | 判断栈是否为空 | O(1) |
size() | 获取栈内元素个数 | O(1) |
适用场景
- 表达式求值(如四则运算、括号匹配);
- 函数调用栈、递归转非递归实现;
- 深度优先遍历(DFS);
- 逆序输出、撤销 / 重做操作。
2. std::queue(队列适配器)
std::queue 是实现先进先出(FIFO, First In First Out)语义的容器适配器,元素只能从队尾(尾部)入队,从队头(头部)出队,只能访问队头和队尾元素,无法访问队列中间的元素。
底层实现
- 默认底层容器:
std::deque(双端队列) - 可选底层容器:
std::list(必须同时支持front()/back()/push_back()/pop_front()四个接口,vector不支持pop_front(),因此无法使用)
核心接口
| 接口 | 作用 | 时间复杂度 |
|---|---|---|
push(val) / emplace(args...) | 元素入队(加入队尾) | O(1) |
pop() | 队头元素出队(无返回值) | O(1) |
front() | 获取队头元素的引用 | O(1) |
back() | 获取队尾元素的引用 | O(1) |
empty() | 判断队列是否为空 | O(1) |
size() | 获取队列元素个数 | O(1) |
适用场景
- 任务调度、消息队列、生产者消费者模型;
- 广度优先遍历(BFS);
- 流量控制、缓冲区管理;
- 按顺序处理的事件队列。
3. std::priority_queue(优先队列适配器)
std::priority_queue 是实现优先级出队语义的容器适配器,每次出队的永远是优先级最高的元素,而非先进先出。底层基于堆(Heap)数据结构实现,默认是大顶堆(值越大优先级越高)。
底层实现
- 默认底层容器:
std::vector - 可选底层容器:
std::deque(必须支持随机访问迭代器、push_back()、pop_back(),list不支持随机访问,因此无法使用) - 核心逻辑:在底层容器上自动调用
std::make_heap/std::push_heap/std::pop_heap堆算法,始终保持堆序性质。
核心接口
| 接口 | 作用 | 时间复杂度 |
|---|---|---|
push(val) / emplace(args...) | 元素入队,自动调整堆结构 | O(logn) |
pop() | 优先级最高的元素出队(堆顶),无返回值 | O(logn) |
top() | 获取堆顶(优先级最高)元素的引用 | O(1) |
empty() | 判断队列是否为空 | O(1) |
size() | 获取队列元素个数 | O(1) |
适用场景
- 任务优先级调度(如操作系统进程调度、事件优先级处理);
- TopK 问题(如找出数组中前 K 大 / 前 K 小的元素);
- 堆排序、哈夫曼编码、最短路径算法(Dijkstra);
- 定时器实现(按超时时间排序)。
函数对象
函数对象是重载了 operator() 的类 / 结构体,它可以像普通函数一样被调用,但比普通函数更强大 —— 可以保存状态、可以作为模板参数、可以内联优化。
std::less 和 std::hash 都是 C++ 标准库预定义的模板函数对象,分别服务于有序关联容器和无序关联容器。
std::less:有序关联容器的默认比较器
std::less 是定义在 <functional> 头文件中的模板函数对象,用于比较两个值的大小,返回「小于」关系的布尔值。
它是所有有序关联容器(map/set/multimap/multiset)的默认比较函数,决定了容器中元素的排序规则。
核心作用:决定有序容器的排序
std::less<int> 会用 int 的 < 运算符比较 key,让容器中的元素按 key 升序排列。
如果默认的 std::less 不满足需求(比如想按降序排列、想按自定义类型的某个成员排序),可以自定义比较函数对象,替换默认的 std::less。
std::hash:无序关联容器的默认哈希函数
std::hash 是定义在 <functional> 头文件中的模板函数对象,用于将一个值(key)映射成一个整数(哈希值) 。
它是所有无序关联容器(unordered_map/unordered_set/unordered_multimap/unordered_multiset)的默认哈希函数,决定了元素在哈希表桶中的存储位置。
标准库默认支持的类型
C++ 标准库只为内置类型和部分标准库类型提供了默认的 std::hash 特化:
- 所有基本数据类型:
int/long/float/double/bool/char等; - 指针类型;
- 部分标准库类型:
std::string/std::string_view/std::unique_ptr/std::shared_ptr等。
注意:标准库没有为自定义类型提供默认的 std::hash 特化,必须自己实现。
核心作用:决定无序容器的元素位置
std::hash<int> 负责将 key 映射成哈希值;
std::equal_to<int> 负责在哈希冲突时判断两个 key 是否相等(默认用 == 运算符)。
如果想用自定义类型作为 unordered_map/unordered_set 的 key,必须做两件事:
特化 std::hash:为类型提供哈希函数;
提供相等比较:要么重载 operator==,要么自定义 KeyEqual 函数对象。