标准模板库(STL)概述:六大组件与容器分类

STL,全称 Standard Template Library,中文叫标准模板库。说实话,我刚学 C++ 那会儿,觉得 STL 就是个黑盒子——用起来爽,但不知道里面怎么运作的。后来做项目多了,才慢慢摸清它的门道。

STL 不是 C++ 语言的一部分,而是 C++ 标准库的核心。它提供了六大组件,彼此配合,构成了一个强大的泛型编程框架。我个人习惯把这六大组件比作一个工厂:容器是仓库,迭代器是传送带,算法是加工机器,仿函数是加工指令,适配器是转换接口,分配器是物料管理员。

六大组件概览

先看一张图,把整体结构理清楚:

STL 六大组件关系图 容器 迭代器 算法 仿函数 适配器 分配器 容器通过迭代器暴露数据,算法通过迭代器操作数据 仿函数定制算法行为,适配器转换接口,分配器管理内存 箭头方向表示「提供支持」或「作用于」的关系

容器(Container)

容器就是用来存放数据的东西。说白了,就是各种数据结构的封装。STL 把容器分成两大类:序列式容器和关联式容器。

序列式容器

序列式容器,元素按插入顺序排列。你放进去什么顺序,取出来就是什么顺序。我最早接触的就是 vector,当时觉得这玩意儿比 C 数组好用太多了。

容器特点适用场景
vector动态数组,连续内存,随机访问 O(1)频繁随机访问,尾部增删
deque双端队列,分段连续,头尾增删 O(1)需要头尾操作
list双向链表,非连续内存,插入删除 O(1)频繁中间插入删除
forward_list单向链表,更省内存只需单向遍历
array固定大小数组,C++11 引入替代 C 风格数组
我的经验: 90% 的场景用 vector 就够了。别一上来就 list,除非你真的需要频繁在中间插入删除。vector 的缓存局部性比 list 好太多,跑起来快一个数量级。

关联式容器

关联式容器,元素按 key 排序或哈希。你放进去的顺序不重要,取的时候按 key 找。这类容器底层通常是红黑树或哈希表。

容器底层结构特点
set / multiset红黑树自动排序,元素唯一(set)或可重复(multiset)
map / multimap红黑树键值对存储,按键排序
unordered_set / unordered_multiset哈希表无序,查找 O(1) 平均
unordered_map / unordered_multimap哈希表无序键值对,查找 O(1) 平均
注意: 关联式容器的迭代器在插入元素时不会失效(哈希表 rehash 时除外),但序列式容器的迭代器在插入/删除时可能失效。我曾经在项目里踩过这个坑——一边遍历 vector 一边插入,结果迭代器全乱了,排查了半天。

迭代器(Iterator)

迭代器是容器和算法之间的桥梁。它把容器的遍历操作抽象成统一的接口。你想想看,如果没有迭代器,每种容器都得写一套自己的遍历代码,那多麻烦。

迭代器按能力分为五类:

  • 输入迭代器:只能读,单向移动。比如从键盘读取数据。
  • 输出迭代器:只能写,单向移动。比如写入文件。
  • 前向迭代器:可读写,单向移动。比如 forward_list 的迭代器。
  • 双向迭代器:可读写,双向移动。比如 list、set、map 的迭代器。
  • 随机访问迭代器:可读写,支持跳跃访问。比如 vector、deque、array 的迭代器。
// 一个简单的例子:用迭代器遍历 vector
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 使用迭代器遍历
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        std::cout << *it << " ";
    }
    // 输出:1 2 3 4 5
    
    return 0;
}
核心思想: 迭代器让算法与容器解耦。同一个 std::sort 可以排序 vector、deque、array,只要它们提供随机访问迭代器。

算法(Algorithm)

STL 提供了上百种算法,涵盖排序、查找、变换、数值计算等。算法不直接操作容器,而是通过迭代器操作。这就是泛型编程的精髓——算法只依赖迭代器的能力,不依赖容器的具体类型。

常用的算法有:

  • std::sort:排序,需要随机访问迭代器
  • std::find:线性查找,输入迭代器即可
  • std::binary_search:二分查找,需要有序序列
  • std::for_each:对每个元素执行操作
  • std::transform:对每个元素进行变换
  • std::copy:复制元素
// 算法示例:排序和查找
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> data = {5, 2, 8, 1, 9};
    
    std::sort(data.begin(), data.end());  // 排序
    
    auto it = std::find(data.begin(), data.end(), 8);
    if (it != data.end()) {
        std::cout << "找到了: " << *it << std::endl;
    }
    
    return 0;
}

仿函数(Functor)

仿函数,说白了就是重载了 operator() 的类对象。它可以像函数一样被调用,但比普通函数灵活——可以保存状态,可以传递参数。

我刚开始觉得仿函数有点多余,直接用函数指针不就行了?后来在项目中需要给 std::sort 传一个带状态的比较器,函数指针搞不定,仿函数就派上用场了。

// 仿函数示例:自定义比较器
struct Descending {
    bool operator()(int a, int b) const {
        return a > b;  // 降序排列
    }
};

std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end(), Descending());
// 结果:5, 4, 3, 1, 1

适配器(Adapter)

适配器用来转换接口。STL 提供了三种容器适配器:

  • stack:栈,后进先出。底层默认用 deque。
  • queue:队列,先进先出。底层默认用 deque。
  • priority_queue:优先队列,最大元素先出。底层默认用 vector。

适配器不提供迭代器,也不支持随机访问。它们只暴露特定的操作接口。说白了,就是给底层容器套了一层限制,让你只能用特定的方式操作数据。

我的建议: 如果你需要栈或队列的功能,直接用适配器,别自己造轮子。适配器代码简洁,而且底层容器可以替换——比如 stack 可以用 vector 或 list 做底层,只要满足接口要求。

分配器(Allocator)

分配器负责内存的分配和释放。默认情况下,STL 容器使用 std::allocator,它调用 newdelete。但在某些场景下,你可能需要自定义分配器,比如:

  • 使用内存池,减少频繁分配的开销
  • 在共享内存中分配数据
  • 对齐特殊类型的内存

说实话,日常开发中很少需要自己写分配器。但理解它的存在很重要——它让 STL 容器可以适应各种内存管理需求。我曾经在一个嵌入式项目里用过自定义分配器,把容器的内存分配从堆改到了静态内存池,解决了内存碎片问题。

// 分配器接口的核心部分(简化)
template<typename T>
class MyAllocator {
public:
    T* allocate(size_t n) {
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }
    void deallocate(T* p, size_t) {
        ::operator delete(p);
    }
    // ... 其他必要接口
};

六大组件如何协作

最后,用一个实际例子看看它们怎么配合:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

int main() {
    // 容器:vector
    std::vector<int> nums = {4, 2, 7, 1, 9, 3};
    
    // 仿函数:greater<int> 是 STL 提供的仿函数
    // 适配器:没有显式使用,但 sort 内部可能使用分配器
    std::sort(nums.begin(), nums.end(), std::greater<int>());
    
    // 迭代器:begin() 和 end() 返回迭代器
    // 算法:for_each
    std::for_each(nums.begin(), nums.end(), [](int n) {
        std::cout << n << " ";
    });
    // 输出:9 7 4 3 2 1
    
    return 0;
}

看到了吗?六大组件各司其职,组合起来就能写出简洁、高效、可复用的代码。这就是 STL 的魅力所在。

总结: 容器存数据,迭代器遍历数据,算法处理数据,仿函数定制行为,适配器转换接口,分配器管理内存。理解这六个角色,你就掌握了 STL 的核心。

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