一、STL容器全家福:先看个全景

STL容器,说白了就是C++标准库给我们准备好的"数据收纳盒"。每个盒子都有自己的脾气和用法。我刚开始学的时候,面对vector、list、map这些名字,说实话有点懵。但用久了你会发现,选对容器,代码写起来顺手得多。

先看一张图,把今天要讲的六个容器串起来:

STL六大容器分类图 序列式容器 元素按顺序排列,像排队 vector 动态数组 list 双向链表 适配器容器 基于其他容器改造而来 stack 后进先出 queue 先进先出 关联式容器 按键值对存储,查找快 map 键值对映射 set 唯一元素集合

这六个容器分三类:序列式容器(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) 不支持 仅队首/队尾操作 不支持 取决于底层容器

九、选型建议:到底用哪个?

我个人的选型原则很简单:

  1. 默认选vector——80%的场景它都够用
  2. 需要频繁中间插入/删除——用list
  3. 需要键值对映射——用map
  4. 需要去重/判断存在——用set
  5. 后进先出——用stack
  6. 先进先出——用queue
一句话总结:vector是瑞士军刀,list是手术刀,map和set是字典和集,stack和queue是专用工具。选对了容器,代码写起来顺手,跑起来也快。

好了,这六个容器就聊到这儿。记住,没有最好的容器,只有最合适的容器。多写多练,慢慢你就会有感觉了。

公众号:蓝海资料掘金营,微信deep3321