23、标准模板库(STL)概览:STL六大组件与容器分类
STL,说白了就是C++标准库的「半壁江山」。我刚开始学C++那会儿,觉得STL就是个黑盒子,用起来爽,但心里没底。后来踩了不少坑,才慢慢摸清它的门道。
今天咱们就把STL的六大组件拆开来看。你想想看,一个程序无非就是「存数据、取数据、处理数据」这三件事。STL把这三大件分得清清楚楚,还加上了几个辅助角色。
STL六大组件:一个都不能少
STL有六个核心组件,我习惯把它们分成三组来记:
- 容器:装数据的「箱子」
- 迭代器:连接容器和算法的「指针」
- 算法:处理数据的「工具」
- 适配器:给容器「换接口」的包装器
- 仿函数:能像函数一样调用的「对象」
- 分配器:管理内存的「幕后管家」
这六个组件配合起来,就像一条流水线。容器负责存,迭代器负责搬运,算法负责加工,适配器负责改装,仿函数负责定制规则,分配器负责后勤保障。
核心思想:STL的设计哲学是「数据与算法分离」。容器只管存数据,算法只管处理数据,迭代器是它们之间的桥梁。这样设计的好处是——你换一种容器,算法照样能用。
容器分类:序列式 vs 关联式
容器是STL里最常用的组件。我把它分成两大类:
序列式容器
序列式容器,说白了就是「排队」——元素按插入顺序排列。你放进去的顺序是什么,取出来就是什么。
| 容器 | 特点 | 适用场景 |
|---|---|---|
vector |
动态数组,尾部插入快,中间插入慢 | 需要随机访问,尾部增删 |
deque |
双端队列,头尾插入都快 | 需要头尾操作 |
list |
双向链表,任意位置插入快 | 频繁中间插入删除 |
forward_list |
单向链表,省内存 | 只需要单向遍历 |
array |
固定大小数组,栈上分配 | 大小已知,不需要动态扩展 |
我在项目中遇到过一个问题:用vector存了10万个元素,然后在中间频繁插入。结果性能直接崩了。后来换成list,问题就解决了。嗯,选对容器真的很重要。
关联式容器
关联式容器不按插入顺序排,而是按「键值」来排。说白了就是「字典」——你给一个键,它直接给你值。
| 容器 | 特点 | 底层结构 |
|---|---|---|
set |
元素唯一,自动排序 | 红黑树 |
map |
键值对,键唯一,自动排序 | 红黑树 |
multiset |
元素可重复,自动排序 | 红黑树 |
multimap |
键可重复,自动排序 | 红黑树 |
unordered_set |
元素唯一,无序 | 哈希表 |
unordered_map |
键值对,无序 | 哈希表 |
为什么会有关联式容器?因为有时候你不想遍历整个容器去找一个元素。比如你有一个用户ID到用户信息的映射,用map就能直接通过ID找到信息,时间复杂度是O(log n)。如果用vector,你得遍历一遍,O(n)。
我的建议:如果你只需要「存一堆东西,然后遍历」,用vector就够了。如果你需要「按某个键快速查找」,用map或unordered_map。别一上来就用list,它虽然插入快,但遍历慢,而且占内存。
迭代器:容器的「通用接口」
迭代器是STL里最巧妙的设计。它把容器和算法解耦了。你想想看,如果没有迭代器,每个容器都得自己实现一套算法——vector_sort、list_sort、map_sort……那代码得多乱?
迭代器有五种类型:
- 输入迭代器:只能读,不能写,比如从键盘读取
- 输出迭代器:只能写,不能读,比如写入文件
- 前向迭代器:可以读写,但只能向前走
- 双向迭代器:可以向前向后走,
list、set、map都支持 - 随机访问迭代器:可以跳着走,
vector、deque、array支持
我曾经犯过一个错:用list的迭代器做it + 5操作。结果编译报错,我才意识到list的迭代器不支持随机访问。嗯,这个坑踩过一次就记住了。
算法:STL的「工具箱」
STL提供了上百种算法,从排序、查找、到复制、变换,应有尽有。我常用的几个:
#include <algorithm>
#include <vector>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
// 排序
std::sort(v.begin(), v.end());
// 查找
auto it = std::find(v.begin(), v.end(), 4);
// 计数
int count = std::count(v.begin(), v.end(), 1);
// 变换
std::transform(v.begin(), v.end(), v.begin(),
[](int x) { return x * 2; });
注意看,这些算法都不直接操作容器,而是通过迭代器。这意味着同一个sort函数,既能排序vector,也能排序deque,甚至能排序array。
适配器:给容器「换壳」
适配器说白了就是「包装」。它把一个容器的接口改成另一个样子。STL提供了三种适配器:
- stack:后进先出,底层默认用
deque - queue:先进先出,底层默认用
deque - priority_queue:优先级队列,底层默认用
vector
你可能会问:为什么不直接用deque?因为stack只允许你从顶部操作,限制了接口,反而更安全。我在写解析器的时候,就用stack来匹配括号,代码清晰多了。
仿函数:像函数一样的对象
仿函数,就是重载了operator()的类。它为什么有用?因为算法需要「规则」,而仿函数可以携带状态。
struct Greater {
bool operator()(int a, int b) const {
return a > b;
}
};
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end(), Greater());
你看,Greater()就是一个仿函数对象。它告诉sort按降序排列。C++11之后,lambda表达式更常用,但仿函数在需要复用规则时还是很有用的。
分配器:内存管理的「幕后英雄」
分配器是STL里最容易被忽略的组件。它负责为容器分配和释放内存。默认的std::allocator用new和delete,但你可以自定义分配器。
我在做嵌入式开发时,内存非常有限。我就写了一个池分配器,预分配一大块内存,然后让容器从池里取。这样避免了频繁的new/delete,性能提升很明显。
注意:自定义分配器是个高级话题。除非你有特殊需求(比如内存池、共享内存),否则用默认的就行。别为了炫技而自定义分配器,容易出bug。
知识体系总览
下面这张图把STL六大组件的关系画清楚了。你可以看到,迭代器是连接容器和算法的桥梁,适配器包装容器,仿函数定制算法规则,分配器为容器提供内存支持。
STL的六大组件,说白了就是一套「积木」。你不需要每次都从零开始造轮子,而是用现成的组件拼出你要的功能。我刚开始用STL时,总觉得它太抽象。后来写多了才发现,它其实是在帮你省时间——你只需要关注业务逻辑,底层的内存管理、数据结构、算法实现,STL都替你搞定了。
嗯,今天就聊到这儿。记住一句话:STL不是黑魔法,它只是一套设计精良的工具。用好了,你的代码会简洁、高效、易维护。
公众号:蓝海资料掘金营,微信deep3321