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)

容器选择决策树

  1. 是否需要键值对存储?
  • 是 → 进入关联式 / 无序关联式容器选择
  • 否 → 进入序列式容器选择
  1. 序列式容器选择:
  • 需要随机访问 → vector(动态)或array(静态)
  • 频繁任意位置插入 / 删除 → list
  • 频繁头尾操作 → deque
  • 字符串处理 → string
  1. 关联式 / 无序关联式容器选择:
  • 需要有序存储 → 红黑树系列(set/map/multiset/multimap)
  • 需要最高查找效率 → 哈希表系列(unordered_set/unordered_map)
  • 允许重复元素 → multiset/multimap
  • 唯一元素 → set/map/unordered_set/unordered_map

四、容器使用最佳实践

  1. 避免不必要的拷贝:对大容器使用引用传递(const&),C++11 后优先使用移动语义(std::move)。
  2. 预分配内存:vector和string使用reserve()预分配容量,unordered_map使用reserve()设置桶数量,减少扩容开销。
  3. 选择合适的迭代器操作:随机访问容器优先使用[]或at(),避免迭代器遍历访问;链表容器避免随机访问。
  4. 注意迭代器失效
  • vector/string:插入可能导致部分或全部迭代器失效,删除导致当前及之后迭代器失效。
  • list:插入不影响迭代器,删除仅导致当前迭代器失效。
  • 关联式容器:插入 / 删除不影响其他元素的迭代器。
  1. 哈希容器优化:为自定义类型设计良好的哈希函数,避免过多哈希冲突;合理设置负载因子(通常 0.7~0.8)。
  2. 优先使用标准容器:除非有特殊需求,否则避免重复实现类似功能的自定义容器,标准容器经过充分优化和测试。

总结

STL 容器为 C++ 开发者提供了丰富的数据结构选择,每种容器都有其独特的底层实现和适用场景。掌握十种常用容器的特性,关键在于理解其底层数据结构对性能的影响(如随机访问、插入 / 删除效率),并根据具体场景的需求(有序性、重复性、操作类型)选择最合适的容器。

实际开发中,需结合数据规模、操作频率和性能要求综合决策,同时遵循最佳实践以避免常见陷阱(如迭代器失效、不必要的拷贝)。通过合理使用 STL 容器,可显著提升代码效率和可维护性。