一、STL容器全家福:先看个全景
STL容器,说白了就是C++标准库给我们准备好的"数据收纳盒"。每个盒子都有自己的脾气和用法。我刚开始学的时候,面对vector、list、map这些名字,说实话有点懵。但用久了你会发现,选对容器,代码写起来顺手得多。
先看一张图,把今天要讲的六个容器串起来:
这六个容器分三类:序列式容器(vector、list)、关联式容器(map、set)、适配器容器(stack、queue)。每类各有各的绝活,我们一个个聊。
二、vector:最常用的"万能选手"
2.1 基本用法
vector就是个动态数组。你不需要提前知道要存多少元素,它会自动扩容。我个人习惯用vector作为默认容器——除非有特殊理由,否则先用vector准没错。
#include <vector>
#include <iostream>
using namespace std;
int main() {
// 创建vector
vector<int> vec;
// 尾部添加元素
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
// 遍历
for (int i = 0; i < vec.size(); ++i) {
cout << vec[i] << " ";
}
cout << endl;
// 使用范围for循环(C++11起)
for (int val : vec) {
cout << val << " ";
}
cout << endl;
return 0;
}
2.2 核心特点
- 连续内存:元素在内存里挨着放,所以访问速度快(O(1)复杂度)
- 尾部操作快:push_back和pop_back都是O(1)
- 中间插入/删除慢:因为要移动后面的元素,O(n)复杂度
- 自动扩容:容量不够时,会申请更大的内存,把旧元素拷贝过去
小技巧:如果你大概知道要存多少元素,可以用reserve()提前分配内存,避免多次扩容带来的性能损耗。比如:vec.reserve(1000);
注意:vector扩容时,所有迭代器、指针、引用都会失效!我曾经在循环里一边遍历一边push_back,结果程序崩溃了。嗯,迭代器失效是个坑,后面会细说。
三、list:频繁插入删除的"好帮手"
3.1 基本用法
list是双向链表。每个元素都带着"前指针"和"后指针",串成一串。插入和删除操作只改指针,不移动数据。
#include <list>
#include <iostream>
using namespace std;
int main() {
list<int> myList;
// 尾部添加
myList.push_back(10);
myList.push_back(20);
// 头部添加
myList.push_front(5);
// 中间插入(在第二个位置插入15)
auto it = myList.begin();
++it; // 指向第二个元素
myList.insert(it, 15);
// 遍历
for (int val : myList) {
cout << val << " ";
}
cout << endl;
return 0;
}
3.2 什么时候用list?
说白了,如果你需要频繁在中间插入或删除元素,就用list。比如实现一个任务队列,任务随时可能插队或取消。
- 优点:任意位置插入/删除都是O(1)(前提是你已经有迭代器指向那个位置)
- 缺点:不支持随机访问,想找第5个元素得从头遍历,O(n)
- 内存开销:每个元素多存两个指针,比vector费内存
我的经验:实际项目中,list的使用频率远低于vector。大多数场景下,vector配合算法库就能搞定。只有当你确定"中间插入删除是性能瓶颈"时,才考虑list。
四、map:键值对的"字典"
4.1 基本用法
map存储键值对(key-value),按键排序,查找速度快。我经常用它做"名字→分数"、"ID→对象"这类映射。
#include <map>
#include <string>
#include <iostream>
using namespace std;
int main() {
map<string, int> scores;
// 插入键值对
scores["Alice"] = 95;
scores["Bob"] = 87;
scores["Charlie"] = 92;
// 或者用insert
scores.insert({"David", 78});
// 查找
auto it = scores.find("Bob");
if (it != scores.end()) {
cout << "Bob的分数: " << it->second << endl;
}
// 遍历(按键排序输出)
for (const auto& pair : scores) {
cout << pair.first << ": " << pair.second << endl;
}
return 0;
}
4.2 核心特点
- 按键排序:默认按key升序排列(底层是红黑树)
- 查找快:O(log n)复杂度
- key唯一:同一个key只能有一个值,后插入的会覆盖前面的
- operator[]的陷阱:如果key不存在,它会自动插入一个默认值
避坑指南:我曾经用map<string, int>,然后写了个if (scores["Tom"] == 0)的判断。结果Tom本来不在map里,这一下反而把Tom插进去了,值为0。正确的做法是用find()先检查是否存在。
五、set:不重复的"集合"
5.1 基本用法
set就是只存key不存value的map。所有元素唯一,自动排序。适合用来去重、判断元素是否存在。
#include <set>
#include <iostream>
using namespace std;
int main() {
set<int> numbers;
numbers.insert(3);
numbers.insert(1);
numbers.insert(4);
numbers.insert(1); // 重复插入,不会生效
numbers.insert(2);
// 遍历(自动排序输出:1 2 3 4)
for (int n : numbers) {
cout << n << " ";
}
cout << endl;
// 判断是否存在
if (numbers.count(3) > 0) {
cout << "3在集合中" << endl;
}
return 0;
}
5.2 什么时候用set?
你想想看,如果你需要维护一个"不重复的名单",或者快速判断某个值是否出现过,set就是最佳选择。比如检查用户名是否已被注册。
- 优点:自动去重、自动排序、查找O(log n)
- 缺点:不能修改元素(因为修改会破坏排序结构)
- 变种:unordered_set用哈希表实现,查找O(1),但不排序
六、stack:后进先出的"栈"
6.1 基本用法
stack是适配器容器,默认基于deque实现。它只允许在栈顶操作:压入、弹出、查看栈顶。说白了,就像一摞盘子,你只能从最上面拿。
#include <stack>
#include <iostream>
using namespace std;
int main() {
stack<int> st;
st.push(10);
st.push(20);
st.push(30);
// 查看栈顶
cout << "栈顶元素: " << st.top() << endl; // 30
// 弹出栈顶
st.pop();
cout << "弹出后栈顶: " << st.top() << endl; // 20
// 遍历(注意:stack不支持迭代器,只能这样弹着看)
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
cout << endl;
return 0;
}
6.2 典型应用
- 函数调用栈(递归)
- 括号匹配检查
- 撤销操作(Ctrl+Z)
- 表达式求值
提示:stack只有push、pop、top、empty、size这几个操作。没有迭代器,不能遍历。如果你需要遍历,说明你选错容器了。
七、queue:先进先出的"队列"
7.1 基本用法
queue也是适配器容器,默认基于deque。就像排队买票,先来的人先服务。
#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<int> q;
q.push(10);
q.push(20);
q.push(30);
// 查看队首和队尾
cout << "队首: " << q.front() << endl; // 10
cout << "队尾: " << q.back() << endl; // 30
// 弹出队首
q.pop();
cout << "弹出后队首: " << q.front() << endl; // 20
return 0;
}
7.2 典型应用
- 任务调度(打印机队列)
- 广度优先搜索(BFS)
- 消息队列
- 缓冲处理
八、六大容器对比总结
| 容器 | 底层结构 | 随机访问 | 中间插入/删除 | 查找 | 内存 |
|---|---|---|---|---|---|
| vector | 动态数组 | O(1) | O(n) | O(n) | 连续,省内存 |
| list | 双向链表 | 不支持 | O(1) | O(n) | 每个元素多两个指针 |
| map | 红黑树 | 不支持 | O(log n) | O(log n) | 较大 |
| set | 红黑树 | 不支持 | O(log n) | O(log n) | 较大 |
| stack | 适配器(deque) | 不支持 | 仅栈顶操作 | 不支持 | 取决于底层容器 |
| queue | 适配器(deque) | 不支持 | 仅队首/队尾操作 | 不支持 | 取决于底层容器 |
九、选型建议:到底用哪个?
我个人的选型原则很简单:
- 默认选vector——80%的场景它都够用
- 需要频繁中间插入/删除——用list
- 需要键值对映射——用map
- 需要去重/判断存在——用set
- 后进先出——用stack
- 先进先出——用queue
一句话总结:vector是瑞士军刀,list是手术刀,map和set是字典和集,stack和queue是专用工具。选对了容器,代码写起来顺手,跑起来也快。
好了,这六个容器就聊到这儿。记住,没有最好的容器,只有最合适的容器。多写多练,慢慢你就会有感觉了。