《STL源码剖析》——侯捷

一流程序员读源码。庖丁解牛,目无全牛。

书籍概览

侯捷经典巨著,中文世界唯一一本深入剖析 STL 底层实现的著作。以 SGI STL 2.91.57 版本为分析对象,从空间配置器到迭代器、容器、算法、仿函数、配接器,逐一拆解 STL 六大组件的源码实现与设计思想。

核心价值:不是教你怎么用 STL,而是教你 STL 为什么这么设计、底层如何实现。读完这本书,你对 STL 的理解将从”会用”跃升至”明理”,甚至达到”能扩展”的第三境界。

阅读门槛:不适合 STL 初学者,更不适合 C++ 初学者。需要有扎实的 C++ 模板基础和 STL 使用经验。

STL 六大组件

组件作用本书章节
空间配置器(Allocator)内存的分配与管理第2章
迭代器(Iterators)连接容器与算法的桥梁,traits 编程技法第3章
容器(Containers)序列式 + 关联式,各种数据结构实现第4-5章
算法(Algorithms)排序、查找、数值、集合运算等第6章
仿函数(Functors)函数对象,算法的策略参数第7章
配接器(Adapters)容器/迭代器/仿函数的包装转换第8章

第1章 STL概论与版本简介

STL 的历史脉络

  • HP 版本:STL 鼻祖,Alexander Stepanov 与 Meng Lee 在 HP 实验室完成的最初实现,所有 STL 版本的祖本
  • P.J. Plauger 版本:Dinkumware 出品,VC++ 自带的 STL 实现,代码可读性较差
  • Rogue Wave 版本:Borland C++ Builder 采用
  • STLport 版本:跨平台移植版,源自 SGI STL,支持多编译器
  • SGI STL 版本:本书分析对象,出自 Stepanov 本人之手,设计最巧妙、思想最深刻,开源免费

SGI STL 文件结构

三类文件:

  1. 标准头文件(无扩展名):<vector>、<list>、<algorithm> 等,用户直接 include
  2. HP 规范头文件(.h 扩展名):C++ Standard 定案前的旧规范
  3. 内部私用文件(stl_*.h):真正的实现代码,如 stl_vector.h、stl_alloc.h

关键 C++ 语法知识点

本章专门铺垫了阅读源码所需的高级 C++ 语法:

  • 模板偏特化(partial specialization):为某些模板参数提供特殊实现
  • 成员模板(member templates):类的成员函数本身也是模板
  • 静态常量整数成员 in-class 初始化:static const int N = 100;
  • 临时对象的产生与运用:int()、vector<int>() 这种显式构造临时对象
  • 前闭后开区间表示法 [):STL 迭代器的核心约定
  • operator() 重载:让类对象可以像函数一样被调用(仿函数的基础)
  • increment/decrement/dereference 操作符:迭代器实现的关键

第2章 空间配置器(Allocator)

STL 一切容器的内存根基。放在最前面讲,是因为理解 allocator 是领悟 STL 工作原理的先决条件。

2.1 标准接口

STL allocator 的标准接口规范:allocate()、deallocate()、construct()、destroy()。

2.2 SGI 双层空间配置器

SGI 设计了两级配置器,核心思想:大内存直接 malloc,小内存用内存池+自由链表管理,避免碎片化。

第一级配置器 __malloc_alloc_template:

  • 处理 >128 字节的大内存申请
  • 直接封装 malloc/free
  • 内置 malloc_alloc_oom_handler 机制:内存不足时调用用户设定的处理函数,反复尝试释放内存后再分配

第二级配置器 __default_alloc_template:

  • 处理 ≤128 字节的小内存申请
  • 维护 16 条 free-lists(8/16/24/…/128 字节各一条)
  • 每次申请上调至 8 的倍数,从对应链表取块
  • 链表空时从内存池(memory pool)批量分配 20 个节点填充
  • 内存池也空了就向系统堆申请一大块(通常 40 个节点量 + 额外附加量)

2.3 内存基本处理工具

三个底层函数,负责在已分配的”原始内存”上构造对象:

函数作用优化策略
uninitialized_copy在 [result, result+n) 上复制构造 [first, last) 的元素POD 类型直接 memcpy,非 POD 逐元素 construct
uninitialized_fill在 [first, last) 上填充值为 x 的对象POD 类型直接 fill,非 POD 逐元素 construct
uninitialized_fill_n在 [first, first+n) 上填充 n 个值为 x 的对象POD 类型直接 fill_n,非 POD 逐元素 construct

核心思想:对 POD(Plain Old Data)类型走最快路径(memcpy/fill),对非 POD 类型走安全路径(逐个构造/析构)。这是 STL 性能优化的经典模式——type_traits + 编译期分发。


第3章 迭代器(Iterators)与 Traits 编程技法

迭代器是 STL 的核心设计:让算法独立于容器,用统一的接口操作不同的数据结构。

3.1 迭代器设计思维

迭代器是一种”智能指针”,封装了对容器元素的访问和移动。算法不直接操作容器,只操作迭代器,实现了算法与容器的解耦。

3.2 Traits 编程技法——STL 源代码门钥

iterator_traits 是萃取迭代器”相应型别”的统一接口。迭代器需要定义 5 种型别:

相应型别含义
value_type迭代器指向的元素类型
difference_type两个迭代器之间的距离类型
reference_type元素的引用类型
pointer_type元素的指针类型
iterator_category迭代器的分类(移动能力等级)

iterator_category 的 5 个等级(继承关系从弱到强):

  1. input_iterator_tag:只读,单向移动
  2. output_iterator_tag:只写,单向移动
  3. forward_iterator_tag:读写,单向移动
  4. bidirectional_iterator_tag:读写,双向移动
  5. random_access_iterator_tag:读写,随机访问(支持 +n、-n、[] 运算)

3.3 编译期分发(Compile-time Dispatching)

以 advance() 和 distance() 为例:

  • advance(it, n):随机访问迭代器可以直接 it += n(O(1)),双向迭代器必须循环 n 次(O(n))
  • distance(first, last):随机访问迭代器可以直接 last - first(O(1)),输入迭代器必须循环计数(O(n))

实现方式:函数重载 + iterator_category 作为参数类型,编译器在编译期选择正确的版本。没有运行时开销。

3.4 std::iterator 保证

所有 STL 迭代器都继承自 std::iterator<category, T, distance, pointer, reference>,自动定义 5 种型别,避免遗漏。

3.5 __type_traits

SGI 的扩展,比 iterator_traits 更进一步——萃取类型本身的特性:

  • has_trivial_default_constructor:是否有平凡默认构造函数
  • has_trivial_copy_constructor:是否有平凡拷贝构造函数
  • has_trivial_assignment_operator:是否有平凡赋值运算符
  • has_trivial_destructor:是否有平凡析构函数
  • is_POD_type:是否是 POD 类型

用途:uninitialized_copy/fill 等底层函数根据这些特性选择最优路径。POD 类型直接 memcpy,非 POD 必须调用构造/析构函数。

这是模板元编程的雏形:在编译期根据类型特性做决策,运行时零开销。


第4章 序列式容器(Sequence Containers)

4.1 容器分类

  • 序列式容器:元素有序排列,位置由插入时机和位置决定
    • vector、list、deque、heap(隐式)、priority_queue、stack、queue、slist

4.2 Vector

底层结构:连续线性空间,三个指针 start、finish、end_of_storage

核心机制——两倍扩容:

  • 容量满时,重新分配两倍大小的新空间
  • 把旧元素全部拷贝/移动到新空间(构造 + 析构)
  • 释放旧空间
  • 所有迭代器、指针、引用全部失效

时间复杂度:

  • push_back 均摊 O(1)(因为扩容是均摊的)
  • 尾部 pop_back O(1)
  • 中间 insert/erase O(n)(元素移动)
  • 随机访问 O(1)

4.3 List

底层结构:双向循环链表,一个哨兵节点 node

特点:

  • 插入/删除 O(1)(只要拿到迭代器)
  • 不连续空间,迭代器不会因插入失效(除了指向被删元素的)
  • 不支持随机访问,没有 operator[]
  • 自带 sort() 成员函数(比 std::sort 高效,因为链表用 merge sort 更合适)

关键操作:splice(拼接)、merge(归并)、reverse(反转)、sort(排序)— 都是 O(n) 但常数因子不同。

4.4 Deque(双端队列)

底层结构:分段连续空间 + 中控器(map,即指针数组)

设计精妙之处:

  • 对外呈现”连续空间”的假象(随机访问迭代器),实际由多段固定大小的缓冲区组成
  • 每段缓冲区大小固定(通常 512 字节或元素大小的倍数)
  • 中控器 map 是一个 T** 数组,每个元素指向一段缓冲区
  • 头尾都可以 O(1) 插入删除(只要当前缓冲区还有空间)
  • 两端扩容时只需要分配新的缓冲区,不需要移动所有元素

迭代器设计复杂:需要维护当前缓冲区位置、缓冲区首尾指针、中控器指针,支持跨缓冲区跳跃。

时间复杂度:

  • 头尾 push/pop 均摊 O(1)
  • 中间 insert/erase O(n)(但比 vector 好,因为只需要移动一端的元素)
  • 随机访问 O(1)(但常数因子比 vector 大)

4.5 Stack 和 Queue

都是容器配接器(container adapter),底层默认用 deque 实现。

  • 没有迭代器:不提供遍历功能,符合栈和队列的语义
  • Stack:LIFO,只有 push/pop/top
  • Queue:FIFO,只有 push/pop/front/back

也可以用 list 作为底层容器(模板参数指定)。

4.6 Heap(堆)

不是容器,是一组算法,底层用 vector + 完全二叉树的隐式表示(数组模拟树)。

四种算法:

  • push_heap:把最后一个元素向上调整(percolate up)
  • pop_heap:把根元素放到最后,把最后元素向下调整(percolate down)
  • sort_heap:反复 pop_heap,得到有序数组
  • make_heap:从乱序数组构建堆(从最后一个非叶节点开始向下调整)

默认是大顶堆(max-heap)。

4.7 Priority Queue

配接器,底层 = vector + heap 算法。默认大顶堆。

没有迭代器,只能访问顶部元素。

4.8 Slist(单向链表)

SGI 扩展,不在 C++ 标准中。单向链表,比 list 省空间(少一个指针),但功能受限。

特殊设计:insert_after / erase_after — 因为单向链表无法快速找到前一个节点,所以操作的是”当前节点的下一个”。


第5章 关联式容器(Associative Containers)

5.1 树的导览

铺垫数据结构知识:二叉搜索树 → AVL 树 → 红黑树。

红黑树的 5 条规则:

  1. 每个节点非红即黑
  2. 根节点是黑色
  3. 红色节点的子节点必须是黑色(不能有连续红节点)
  4. 从根到 NULL 的每条路径上黑节点数目相同
  5. NULL 节点视为黑色

红黑树的优势:插入/删除/查找都是 O(log n),且通过旋转和变色维持平衡,最坏情况也不会退化。

5.2 RB-tree(红黑树)

SGI 所有标准关联式容器(set、map、multiset、multimap)的底层都是红黑树。

关键设计:

  • 节点结构:color、parent、left、right、value
  • 迭代器:中序遍历(in-order),所以关联式容器天然有序
  • insert_unique:不允许键值重复
  • insert_equal:允许键值重复

5.3 Set / Map / Multiset / Multimap

容器键值关系底层可否重复
set键 = 值RB-tree不可
map键值对(pair)RB-tree键不可重复
multiset键 = 值RB-tree可重复
multimap键值对(pair)RB-tree键可重复

共同特点:

  • 元素自动按 key 排序(中序遍历红黑树)
  • 查找 O(log n)
  • 插入删除 O(log n) + 平衡调整
  • map 的 operator[] 语义:key 不存在则插入默认值

5.4 Hashtable(哈希表)

SGI 扩展,不在 C++98 标准中(C++11 引入 unordered 系列)。

冲突处理:开链法(separate chaining),每个桶(bucket)挂一个链表。

关键参数:

  • bucket 数量:总是质数(优化哈希分布)
  • 负载因子(load factor):元素数 / bucket 数 > 1 时触发 resize,bucket 数翻倍到下一个质数
  • hash function:对整数类型直接取模,对字符串等有专门的哈希函数

5.5 hash_set / hash_map / hash_multiset / hash_multimap

底层用 hashtable 实现的关联式容器。

与 RB-tree 版本的对比:

  • 查找平均 O(1),最坏 O(n)(全冲突)
  • 元素无序(不排序)
  • 需要好的 hash 函数

第6章 算法(Algorithms)

6.1 算法分类

质变算法(mutating):会改变元素值/顺序

  • copy、fill、swap、sort、reverse、rotate、remove、replace、transform…

非质变算法(non-mutating):不改变元素

  • find、count、search、for_each、equal、mismatch、min、max…

6.2 算法的泛化过程

从”针对特定数据结构的函数”进化到”针对迭代器的通用算法”——这就是泛型编程的本质。

6.3 数值算法 <stl_numeric.h>

算法作用
accumulate累加(可指定初值和操作)
adjacent_difference相邻差分
inner_product内积(点积)
partial_sum部分和(前缀和)
power快速幂(SGI 扩展,用二分法幂运算)
itoa生成连续递增序列(SGI 扩展)

6.4 基本算法 <stl_algobase.h>

equal、fill、fill_n、iter_swap、lexicographical_compare、max/min、mismatch、swap。

重点:copy 算法的极致优化:

  • 输入输出都是随机访问迭代器 → 按块复制
  • 输入是 const char*/wchar_t* → 直接 memmove
  • 输出是 char*/wchar_t* → 直接 memmove
  • POD 类型 → memmove
  • 非 POD 类型 → 逐元素赋值

层层特化、层层优化,无所不用其极,这就是工业级库的水准。

6.5 Set 相关算法

set_union、set_intersection、set_difference、set_symmetric_difference

前提:两个输入区间都必须已排序。时间复杂度 O(m+n)。

6.6 其他重要算法

算法说明时间复杂度
sort混合排序( introsort = quicksort + heapsort + insertion sort )O(n log n)
stable_sort归并排序,稳定O(n log n)
partial_sort部分排序(堆排序思想)O(n log k),k 是前 k 个
nth_element第 n 小元素(快速选择思想)平均 O(n)
binary_search二分查找O(log n)
lower_bound / upper_bound第一个 ≥ / > target 的位置O(log n)
equal_range等于 target 的区间O(log n)
merge归并两个有序区间O(m+n)
inplace_merge原地归并O(n) ~ O(n log n)
reverse反转区间O(n)
rotate旋转区间O(n),三种实现策略
next_permutation / prev_permutation下一个/上一个排列O(n)
random_shuffle随机打乱O(n)
for_each对每个元素执行操作O(n)
find / find_if线性查找O(n)
count / count_if计数O(n)
search子串查找O(m*n)

sort 的设计:SGI 的 sort 是 Introsort(内省排序):

  1. 默认用快速排序(递归深度不大时)
  2. 递归深度超过 log₂n 阈值时,切换到堆排序(避免最坏情况 O(n²))
  3. 子区间长度 < 阈值(通常 16)时,停止排序,最后用插入排序收尾(小数组插入排序更快)

这是多种算法结合、扬长避短的典范。


第7章 仿函数(Functors)

仿函数就是重载了 operator() 的类对象,可以像函数一样被调用。比函数指针更灵活,可以保存状态,可以被配接。

7.1 可配接的关键

仿函数要能被 STL 配接器(bind2nd、not1 等)操作,必须定义自己的型别:

  • unary_function<Arg, Result>:一元仿函数的基类,定义 argument_type 和 result_type
  • binary_function<Arg1, Arg2, Result>:二元仿函数的基类,定义 first_argument_type、second_argument_type、result_type

这是 traits 编程思想的延伸——通过继承获得统一的型别定义。

7.2 算术类仿函数

plus、minus、multiplies、divides、modulus、negate

7.3 关系运算类仿函数

equal_to、not_equal_to、greater、less、greater_equal、less_equal

less<T> 是最常用的,几乎所有关联式容器和排序算法默认都用 it。

7.4 逻辑运算类仿函数

logical_and、logical_or、logical_not

7.5 证同、选择、投射

  • identity:返回自身(set 中 key=value 时用)
  • select1st / select2nd:取 pair 的第一个/第二个元素(map 中取 key/value 时用)
  • project1st / project2nd:忽略第二个/第一个参数,返回第一个/第二个

这些看似简单的”工具仿函数”,是 STL 组件灵活组合的关键。


第8章 配接器(Adapters)

配接器是一种设计模式:把一个接口转换成另一个接口,让原本不兼容的类可以合作。

8.1 分类

  1. 容器配接器(container adapters):stack、queue,把 deque 等容器包装成特定语义
  2. 迭代器配接器(iterator adapters):insert iterators、reverse iterators、stream iterators
  3. 仿函数配接器(functor adapters):bind2nd、not1、compose1 等

8.2 迭代器配接器

Insert Iterators(插入迭代器):

  • back_insert_iterator(绑定 push_back)→ back_inserter()
  • front_insert_iterator(绑定 push_front)→ front_inserter()
  • insert_iterator(绑定 insert)→ inserter()

把”赋值”操作转化为”插入”操作。算法输出到插入迭代器时,元素自动被插入到容器中。

Reverse Iterators(反向迭代器):

  • 把 ++ 变成 —,把 — 变成 ++
  • 注意:反向迭代器的”当前元素”是其内部正向迭代器的前一个元素(为了保持前闭后开区间的一致性)
  • base() 成员函数可以获取对应的正向迭代器

Stream Iterators(流迭代器):

  • istream_iterator:把输入流包装成输入迭代器
  • ostream_iterator:把输出流包装成输出迭代器
  • 可以直接把算法和 I/O 流连起来

8.3 仿函数配接器

配接器作用
not1 / not2对返回值取反(一元/二元)
bind1st / bind2nd绑定第一个/第二个参数,把二元仿函数变成一元
compose1 / compose2函数合成(SGI 扩展)
ptr_fun把函数指针包装成可配接的仿函数
mem_fun / mem_fun_ref把成员函数指针包装成仿函数

这些配接器是函数式编程思想在 C++ 中的早期体现。C++11 之后被 lambda 和 std::bind 取代,但理解它们的设计对理解泛型编程思想很有帮助。


附录

  • 附录A:参考书籍与推荐读物(泛型思维理论基础 + STL 实务应用)
  • 附录B:侯捷网站导引
  • 附录C:STLport 的移植经验(孟岩)— Borland C++Builder 5 / VC++ 6.0

核心思想总结

1. 泛型编程(Generic Programming)

STL 的灵魂。不是面向对象(OOP),而是泛型编程——用模板让算法和数据结构独立于具体类型,用迭代器让算法独立于具体容器。

“发现算法的共性,抽象出最小依赖,让同一个算法可以操作最大范围的类型。“

2. 分层设计 + 组合优于继承

六大组件各司其职,通过迭代器和仿函数连接。组合的灵活性远大于继承。

3. 零开销抽象(Zero-Cost Abstraction)

STL 的抽象几乎没有运行时开销:

  • 模板 = 编译期生成代码,没有虚函数开销
  • traits = 编译期型别分发,没有运行时判断
  • 内联函数消除函数调用开销
  • POD 类型特化直接走 memcpy,比手写循环还快

4. 内存管理哲学

  • 把内存配置和对象构造分开(allocate + construct)
  • 小内存用内存池管理,减少 malloc 开销和碎片
  • POD 类型走最快路径

5. 约定优于配置

迭代器的 5 种型别、前闭后开区间、迭代器分类等级… 这些是 STL 世界的”契约”,遵守约定的组件可以自由组合。


与其他笔记的关联

阅读建议

  1. 第一遍:通读第1-3章,理解 STL 整体架构和 traits 技法
  2. 第二遍:选最感兴趣的 2-3 个容器(推荐 vector、list、deque),对着源码一步步读
  3. 第三遍:读算法章,重点关注 sort、copy、rotate 这几个”算法设计教科书”级别的实现
  4. 实践:尝试自己实现一个简单的 STL 容器或算法,比如写一个 vector 或者自己的 allocator

侯捷说:STL 学习有三重境界——会用、明理、能扩展。这本书带你从第一重直贯第二重,渐达第三重。