C++ STL容器详解:从入门到精通
C++ STL容器详解:从入门到精通
一、STL容器概述
STL(Standard Template Library,标准模板库)是C++标准库的核心组件,提供了一套高效、可复用的数据结构和算法。STL容器作为其重要组成部分,用于存储和管理数据集合,遵循泛型编程思想,通过模板实现类型无关性。
STL核心组件
- 容器(Containers):存放数据的结构,如vector、list、map等
- 算法(Algorithms):如sort、find、accumulate等(头文件<algorithm>)
- 迭代器(Iterators):连接算法与容器的"指针风格"对象
- 函数对象/谓词:比较器、定制规则(常用lambda表达式)
- 适配器:容器/迭代器/函数的包装器
二、STL容器分类体系
STL容器按照数据组织方式分为三大类:
1. 序列容器(Sequence Containers)
元素按插入顺序存储,保持线性顺序:
- vector:动态数组,连续内存存储
- deque:双端队列,支持头尾高效操作
- list:双向链表,任意位置插入删除高效
- forward_list:单向链表(C++11)
- array:固定长度数组(C++11)
2. 关联容器(Associative Containers)
元素按键值自动排序,基于红黑树实现:
- set:唯一元素的有序集合
- multiset:允许重复元素的有序集合
- map:键值对映射,键唯一
- multimap:允许重复键的映射
3. 无序关联容器(Unordered Associative Containers)
基于哈希表实现,元素无序,查找效率高(C++11):
- unordered_set:哈希集合
- unordered_multiset:允许重复的哈希集合
- unordered_map:哈希映射
- unordered_multimap:允许重复键的哈希映射
4. 容器适配器(Container Adapters)
基于其他容器提供特定功能接口:
- stack:栈(后进先出)
- queue:队列(先进先出)
- priority_queue:优先队列
三、序列容器详解
1. vector(动态数组)
核心特性
- 底层实现:连续内存的动态数组
- 随机访问:O(1)时间复杂度
- 尾部操作:push_back/pop_back均摊O(1)
- 中间操作:插入/删除O(n)
- 内存管理:自动扩容(通常按1.5或2倍增长)
常用操作
#include <vector> #include <iostream> int main() { // 多种初始化方式 std::vector<int> v1; // 空vector std::vector<int> v2(5); // 5个0 std::vector<int> v3(5, 10); // 5个10 std::vector<int> v4 = {1, 2, 3, 4, 5}; // 初始化列表 // 容量管理 v1.reserve(100); // 预留100个元素空间 std::cout << "size: " << v4.size() << std::endl; // 当前元素数量 std::cout << "capacity: " << v4.capacity() << std::endl; // 当前容量 // 访问元素 std::cout << "front: " << v4.front() << std::endl; // 第一个元素 std::cout << "back: " << v4.back() << std::endl; // 最后一个元素 std::cout << "at(2): " << v4.at(2) << std::endl; // 安全访问 // 修改操作 v4.push_back(6); // 尾部添加 v4.insert(v4.begin() + 2, 100); // 中间插入 v4.erase(v4.begin() + 1); // 删除 v4.pop_back(); // 删除尾部 // 高级操作 std::sort(v4.begin(), v4.end()); // 排序 v4.resize(10, 0); // 调整大小 return 0; }性能优化技巧
// 预分配避免频繁扩容 void vector_reserve_demo() { // ❌ 错误做法:不预分配 std::vector<int> bad_vec; for (int i = 0; i < 10000; ++i) { bad_vec.push_back(i); // 可能触发多次内存重新分配 } // ✅ 正确做法:预分配容量 std::vector<int> good_vec; good_vec.reserve(10000); // 一次性分配足够内存 for (int i = 0; i < 10000; ++i) { good_vec.push_back(i); // 不会触发重新分配 } } // 高效删除元素 void vector_erase_demo() { std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // ❌ 低效:从前往后删除 for (auto it = vec.begin(); it != vec.end();) { if (*it % 2 == 0) { it = vec.erase(it); // O(n)操作,总体O(n²) } else { ++it; } } // ✅ 高效:使用remove_if + erase vec.erase( std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }), vec.end() ); // O(n)操作 } // 使用emplace避免不必要的拷贝 void modern_vector_usage() { std::vector<std::pair<int, std::string>> vec; // ❌ 创建临时对象再拷贝 vec.push_back(std::make_pair(1, "hello")); vec.push_back({2, "world"}); // ✅ 直接在容器中构造对象 vec.emplace_back(3, "modern"); // 直接构造,避免拷贝 vec.emplace_back(4, "cpp"); }2. list(双向链表)
核心特性
- 底层实现:双向链表,元素非连续存储
- 任意位置操作:插入/删除O(1)
- 随机访问:O(n),需遍历
- 内存开销:每个节点需额外存储前后指针
常用操作
#include <list> #include <iostream> int main() { std::list<int> numbers = {1, 2, 3, 4, 5}; // 添加元素 numbers.push_back(10); numbers.push_front(0); // 中间插入 auto it = numbers.begin(); std::advance(it, 2); // 找到第3个位置 numbers.insert(it, 100); // O(1)插入 // 删除元素 numbers.erase(numbers.begin()); numbers.pop_back(); // 链表特有操作 numbers.sort(); // 链表排序 numbers.reverse(); // 反转链表 numbers.unique(); // 去重(需先排序) // 遍历 for (auto num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; }适用场景
- 需要频繁在中间位置插入删除元素
- 对随机访问要求不高
- 需要高效的链表合并操作(splice)
3. deque(双端队列)
核心特性
- 底层实现:分段连续存储(多段数组)
- 头尾操作:push_front/push_back均为O(1)
- 随机访问:O(1),但比vector略慢
- 中间操作:插入/删除O(n)
常用操作
#include <deque> #include <iostream> int main() { std::deque<int> dq; // 双端操作 dq.push_front(1); // 头部插入 dq.push_back(2); // 尾部插入 dq.push_front(0); // 头部插入 dq.push_back(3); // 尾部插入 // 随机访问 std::cout << "第一个元素: " << dq[0] << std::endl; std::cout << "最后一个元素: " << dq[dq.size()-1] << std::endl; // 删除操作 dq.pop_front(); // 头部删除 dq.pop_back(); // 尾部删除 return 0; }适用场景
- 需要频繁在两端插入删除
- 偶尔需要随机访问
- 实现滑动窗口算法
- 双向缓冲区
4. array(固定数组)
核心特性
- 底层实现:编译期定长的连续数组
- 随机访问:O(1)
- 大小固定:无法动态扩容
- 安全访问:提供at()方法进行边界检查
常用操作
#include <array> #include <iostream> int main() { std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 访问元素 std::cout << "第一个元素: " << arr[0] << std::endl; std::cout << "最后一个元素: " << arr.back() << std::endl; // 安全访问 try { std::cout << arr.at(10) << std::endl; // 抛出异常 } catch (const std::out_of_range& e) { std::cout << "越界访问: " << e.what() << std::endl; } // 遍历 for (auto num : arr) { std::cout << num << " "; } std::cout << std::endl; return 0; }适用场景
- 元素数量固定且已知
- 需要替代原生数组,提供更安全的接口
- 存储配置信息、坐标等固定长度数据
四、关联容器详解
1. set/multiset(集合)
核心特性
- 底层实现:红黑树(平衡二叉搜索树)
- 自动排序:元素按升序排列
- 查找效率:O(log n)
- 唯一性:set不允许重复,multiset允许重复
常用操作
#include <set> #include <iostream> int main() { // set:唯一元素 std::set<int> unique_set = {3, 1, 4, 1, 5}; // {1, 3, 4, 5} // multiset:允许重复 std::multiset<int> multi_set = {3, 1, 4, 1, 5}; // {1, 1, 3, 4, 5} // 插入元素 unique_set.insert(2); unique_set.insert(3); // 重复,不会插入 // 查找元素 auto it = unique_set.find(3); if (it != unique_set.end()) { std::cout << "找到元素: " << *it << std::endl; } // 删除元素 unique_set.erase(4); // 遍历 for (auto num : unique_set) { std::cout << num << " "; } std::cout << std::endl; // 范围查找 auto lower = unique_set.lower_bound(2); // >=2的最小元素 auto upper = unique_set.upper_bound(4); // >4的最小元素 return 0; }适用场景
- 需要去重且有序的集合
- 频繁查找、插入、删除操作
- 需要范围查询(lower_bound/upper_bound)
2. map/multimap(映射)
核心特性
- 底层实现:红黑树,存储键值对
- 自动排序:按键值升序排列
- 查找效率:O(log n)
- 唯一性:map键唯一,multimap允许重复键
常用操作
#include <map> #include <iostream> #include <string> int main() { std::map<std::string, int> student_scores; // 插入元素 student_scores["Alice"] = 95; student_scores["Bob"] = 87; student_scores["Charlie"] = 92; // 安全插入:只有键不存在时才插入 auto result = student_scores.insert({"David", 88}); if (result.second) { std::cout << "David插入成功" << std::endl; } // emplace:直接在容器内构造对象 student_scores.emplace("Eve", 91); // 查找的多种方式 // 方式1:使用find(推荐) auto it = student_scores.find("Alice"); if (it != student_scores.end()) { std::cout << "Alice的分数: " << it->second << std::endl; } // 方式2:使用count + operator[] if (student_scores.count("Bob")) { std::cout << "Bob的分数: " << student_scores["Bob"] << std::endl; } // 删除元素 student_scores.erase("Charlie"); // 遍历 for (const auto& pair : student_scores) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }适用场景
- 键值对存储,需要按键快速查找
- 需要有序遍历键值对
- 实现字典、配置表、计数器等
五、无序关联容器详解
1. unordered_set/unordered_map
核心特性
- 底层实现:哈希表
- 查找效率:平均O(1),最坏O(n)
- 元素无序:不保持插入顺序
- 自定义类型:需提供哈希函数和相等比较函数
常用操作
#include <unordered_map> #include <iostream> #include <string> // 自定义类型的哈希函数 struct Person { std::string name; int age; bool operator==(const Person& other) const { return name == other.name && age == other.age; } }; namespace std { template<> struct hash<Person> { size_t operator()(const Person& p) const { return hash<string>()(p.name) ^ hash<int>()(p.age); } }; } int main() { // unordered_map基本用法 std::unordered_map<std::string, int> word_count; // 插入元素 word_count["hello"] = 1; word_count["world"] = 2; word_count["hello"]++; // 更新 // 查找元素 auto it = word_count.find("hello"); if (it != word_count.end()) { std::cout << "hello出现次数: " << it->second << std::endl; } // 遍历(无序) for (const auto& pair : word_count) { std::cout << pair.first << ": " << pair.second << std::endl; } // 自定义类型使用 std::unordered_map<Person, int> person_map; person_map[{ "Alice", 25 }] = 100; return 0; }适用场景
- 需要极速查找,不关心元素顺序
- 高频查询的缓存系统
- 计数器、频率统计
- 去重操作
六、容器适配器详解
1. stack(栈)
核心特性
- 底层实现:基于deque或list
- 后进先出:LIFO(Last-In-First-Out)
- 操作限制:只能在栈顶插入删除
常用操作
#include <stack> #include <iostream> int main() { std::stack<int> s; // 入栈 s.push(1); s.push(2); s.push(3); // 访问栈顶 std::cout << "栈顶元素: " << s.top() << std::endl; // 出栈 s.pop(); std::cout << "出栈后栈顶: " << s.top() << std::endl; // 栈大小 std::cout << "栈大小: " << s.size() << std::endl; return 0; }适用场景
- 函数调用栈
- 表达式求值
- 括号匹配
- 深度优先搜索(DFS)
2. queue(队列)
核心特性
- 底层实现:基于deque或list
- 先进先出:FIFO(First-In-First-Out)
- 操作限制:队尾插入,队头删除
常用操作
#include <queue> #include <iostream> int main() { std::queue<int> q; // 入队 q.push(1); q.push(2); q.push(3); // 访问队头 std::cout << "队头元素: " << q.front() << std::endl; // 出队 q.pop(); std::cout << "出队后队头: " << q.front() << std::endl; // 访问队尾 std::cout << "队尾元素: " << q.back() << std::endl; return 0; }适用场景
- 任务调度
- 消息队列
- 广度优先搜索(BFS)
- 缓存管理
3. priority_queue(优先队列)
核心特性
- 底层实现:基于vector的二叉堆(最大堆)
- 优先级排序:默认最大元素在队首
- 操作效率:push/pop为O(log n),top为O(1)
常用操作
#include <queue> #include <iostream> #include <functional> // 用于greater int main() { // 默认最大堆 std::priority_queue<int> max_heap; max_heap.push(3); max_heap.push(1); max_heap.push(4); max_heap.push(2); std::cout << "最大堆: "; while (!max_heap.empty()) { std::cout << max_heap.top() << " "; // 4 3 2 1 max_heap.pop(); } std::cout << std::endl; // 最小堆 std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap; min_heap.push(3); min_heap.push(1); min_heap.push(4); min_heap.push(2); std::cout << "最小堆: "; while (!min_heap.empty()) { std::cout << min_heap.top() << " "; // 1 2 3 4 min_heap.pop(); } std::cout << std::endl; return 0; }适用场景
- 任务优先级调度
- 求Top K问题
- 堆排序
- Dijkstra算法等图算法
七、容器性能对比与选型指南
1. 时间复杂度对比
操作 | vector | deque | list | map/set | unordered_map/set |
|---|---|---|---|---|---|
随机访问 | O(1) | O(1) | O(n) | O(log n) | O(1)平均 |
尾部插入 | O(1)摊销 | O(1) | O(1) | O(log n) | O(1)平均 |
头部插入 | O(n) | O(1) | O(1) | O(log n) | O(1)平均 |
中间插入 | O(n) | O(n) | O(1) | O(log n) | O(1)平均 |
查找 | O(n) | O(n) | O(n) | O(log n) | O(1)平均 |
删除 | O(n) | O(n) | O(1) | O(log n) | O(1)平均 |
2. 选型原则
选型口诀
- 随机访问选vector:需要快速随机访问,尾部操作多
- 头尾操作多选deque:需要频繁在两端插入删除
- 中间操作频繁选list:需要任意位置高效插入删除
- 快速查找选unordered:不关心顺序,追求极致查找效率
- 有序需求选map/set:需要有序遍历、范围查询
常见场景推荐
- 90%场景:优先选择vector,通用性强
- 查找场景:unordered_map/unordered_set(O(1)查找)
- 有序场景:map/set(需要排序或范围查询)
- 频繁插入删除:list或deque(根据位置选择)
- 队列/栈:queue/stack(基于deque)
3. 性能实测数据
根据实际测试(100万次操作):
操作类型 | vector | deque | list | unordered_map |
|---|---|---|---|---|
顺序访问 | ≈1ms | ≈2ms | ≈150ms | - |
尾部插入 | ≈2ms | ≈2ms | ≈8ms | - |
中间插入 | ≈150ms | ≈50ms | ≈1ms | - |
查找操作 | - | - | ≈500ms | ≈1ms |
八、STL容器内存管理
1. 内存分配机制
STL容器使用分配器(Allocator)管理内存,默认使用std::allocator。容器在需要时动态分配内存,自动处理内存的分配和释放。
vector扩容策略
void vector_memory_analysis() { std::vector<int> vec; std::cout << "空vector大小: " << sizeof(vec) << "字节" << std::endl; for (int i = 1; i <= 5; ++i) { vec.push_back(i); std::cout << "元素数: " << vec.size() << ", 容量: " << vec.capacity() << std::endl; } }输出结果(不同编译器可能不同):
空vector大小: 24字节 元素数: 1, 容量: 1 元素数: 2, 容量: 2 元素数: 3, 容量: 4 元素数: 4, 容量: 4 元素数: 5, 容量: 82. 内存优化技巧
预分配容量
// ❌ 错误做法:频繁扩容 std::vector<int> bad_vec; for (int i = 0; i < 10000; ++i) { bad_vec.push_back(i); // 可能触发多次内存重新分配 } // ✅ 正确做法:预分配 std::vector<int> good_vec; good_vec.reserve(10000); // 一次性分配足够内存 for (int i = 0; i < 10000; ++i) { good_vec.push_back(i); // 不会触发重新分配 }释放多余内存
std::vector<int> vec; vec.reserve(1000); // ... 添加元素后,实际只用了100个 vec.shrink_to_fit(); // 释放多余内存3. 自定义分配器
#include <vector> #include <memory> #include <iostream> template<typename T> class MyAllocator : public std::allocator<T> { public: // 重写allocate和deallocate T* allocate(std::size_t n) { std::cout << "Allocating " << n << " elements" << std::endl; return std::allocator<T>::allocate(n); } void deallocate(T* p, std::size_t n) { std::cout << "Deallocating " << n << " elements" << std::endl; std::allocator<T>::deallocate(p, n); } }; int main() { std::vector<int, MyAllocator<int>> vec; vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.pop_back(); return 0; }九、常见误区与最佳实践
1. 常见误区
误区1:list查找也不慢?
- 事实:list查找是O(n),只有插入删除快
- 建议:除非需要频繁中间插入删除,否则优先选择vector
误区2:map查找就是快?
- 事实:unordered_map查找是O(1),比map的O(log n)更快
- 建议:不关心顺序时优先选择unordered_map
误区3:容器默认线程安全?
- 事实:STL默认不线程安全
- 建议:多线程环境需加锁或使用并发容器
误区4:vector扩容特别慢?
- 事实:vector扩容是摊销O(1),不要过度担心
- 建议:合理使用reserve()预分配即可
2. 最佳实践
迭代器失效问题
std::vector<int> vec = {1, 2, 3, 4, 5}; auto it = vec.begin(); // ❌ 错误:插入可能导致扩容,迭代器失效 vec.push_back(6); // std::cout << *it << std::endl; // 未定义行为 // ✅ 正确:重新获取迭代器 it = vec.begin(); std::cout << *it << std::endl; // 安全使用emplace避免拷贝
std::vector<std::string> vec; // ❌ 创建临时对象再拷贝 vec.push_back(std::string("hello")); // ✅ 直接在容器中构造对象 vec.emplace_back("hello"); // 避免拷贝合理选择删除方式
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // ❌ 低效:从前往后删除 for (auto it = vec.begin(); it != vec.end();) { if (*it % 2 == 0) { it = vec.erase(it); // O(n)操作,总体O(n²) } else { ++it; } } // ✅ 高效:使用remove_if + erase vec.erase( std::remove_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }), vec.end() ); // O(n)操作十、总结
STL容器是C++编程中不可或缺的工具,选择合适的容器能显著提升程序性能。掌握各容器的特性、时间复杂度、适用场景,是成为高效C++程序员的关键。
核心要点总结:
- vector是90%场景的首选:随机访问快,尾部操作高效
- unordered系列查找最快:不关心顺序时优先选择
- list只适合极少数场景:频繁中间插入删除才考虑
- 合理使用reserve预分配:避免vector频繁扩容
- 理解迭代器失效规则:插入删除操作后需重新获取迭代器
通过本文的系统学习,相信你已经掌握了STL容器的核心知识。在实际开发中,根据具体需求选择合适的容器,配合性能分析工具验证效果,才能写出既高效又健壮的C++代码。