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就够了。如果你需要「按某个键快速查找」,用mapunordered_map。别一上来就用list,它虽然插入快,但遍历慢,而且占内存。

迭代器:容器的「通用接口」

迭代器是STL里最巧妙的设计。它把容器和算法解耦了。你想想看,如果没有迭代器,每个容器都得自己实现一套算法——vector_sortlist_sortmap_sort……那代码得多乱?

迭代器有五种类型:

  • 输入迭代器:只能读,不能写,比如从键盘读取
  • 输出迭代器:只能写,不能读,比如写入文件
  • 前向迭代器:可以读写,但只能向前走
  • 双向迭代器:可以向前向后走,listsetmap都支持
  • 随机访问迭代器:可以跳着走,vectordequearray支持

我曾经犯过一个错:用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::allocatornewdelete,但你可以自定义分配器。

我在做嵌入式开发时,内存非常有限。我就写了一个池分配器,预分配一大块内存,然后让容器从池里取。这样避免了频繁的new/delete,性能提升很明显。

注意:自定义分配器是个高级话题。除非你有特殊需求(比如内存池、共享内存),否则用默认的就行。别为了炫技而自定义分配器,容易出bug。

知识体系总览

下面这张图把STL六大组件的关系画清楚了。你可以看到,迭代器是连接容器和算法的桥梁,适配器包装容器,仿函数定制算法规则,分配器为容器提供内存支持。

STL六大组件关系图 容器 vector, list, map, set... 存数据 迭代器 begin(), end(), ++, * 连接容器和算法 算法 sort, find, count... 处理数据 提供 驱动 适配器 stack, queue, priority_queue 仿函数 greater, less, 自定义 分配器 allocator, 自定义 包装 定制规则 分配内存 迭代器是核心枢纽,连接容器和算法 适配器、仿函数、分配器提供扩展能力

STL的六大组件,说白了就是一套「积木」。你不需要每次都从零开始造轮子,而是用现成的组件拼出你要的功能。我刚开始用STL时,总觉得它太抽象。后来写多了才发现,它其实是在帮你省时间——你只需要关注业务逻辑,底层的内存管理、数据结构、算法实现,STL都替你搞定了。

嗯,今天就聊到这儿。记住一句话:STL不是黑魔法,它只是一套设计精良的工具。用好了,你的代码会简洁、高效、易维护。


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