C++ STL标准库源码解析与设计思想
最近在学习侯捷老师关于STL源码的解析以GNU2.9实现为例),本文记录核心知识点与设计哲学
一、STL六大组件全景图
STL主要分成了六个部分,其中包括容器,迭代器,分配器,算法,仿函数以及一些适配器。在学习STL之前应该首先对泛型编程以及对象编程有清晰的认识,对象编程倾向于设计一个Class类,实现一个具备自身数据和自身功能的一个整体,并提供复用的接口。泛型编程广泛的应用了模板的相关知识,通过模板参数T或者Foo来分别设计函数和数据的具体实现。对于不同版本的库文件,各家的编写方式也不尽相同,例如VC库文件和GNU库文件。同时由于标准规范存在,代码能够有较好的可移植性
STL(Standard Template Library)由以下六大核心组件构成:
- 容器(Containers):管理数据的集合(如vector、list、map)
- 迭代器(Iterators):泛化的指针,提供容器元素的访问接口
- 分配器(Allocators):内存管理的底层实现(如
std::alloc) - 算法(Algorithms):通用算法(如sort、find)
- 仿函数(Functors):行为类似函数的对象(如
less<T>) - 适配器(Adapters):组件接口转换器(如stack、queue)
- 容器主要分成关联式和序列式容器,对于序列是容器能够支持一些排序等操作。而关联式容器支持快速的查找工作
在学习容器部分的时候应该格外关注他们的底层实现,这关乎于不同算法的实现效率,另外以及内存占用情况,以及容器内部定义的可以调用的一些基本函数。注意容器内部定义的一些函数和全局定义的函数的区别,例如find、sort等函数。 - 对于vector是使用三个指针进行控制,头部指针,内容尾部指针,以及内存尾部指针。vecotr的迭代器,不必设计成一个类。
- deque是使用分段连续实现前后端均可以扩充对于deque的迭代器,使用四个属性进行控制。cur.first.last,node,deque的底层索引表也是通过vector来写的,进行二倍增长
- stack 和queue的底层都是采用qeque来实现的。两者都不允许进行遍历,那么就是不提供迭代器
- 不同的容器其本身由于包括了多个迭代器,其指针等其本身就占用多个字节,例如deque占用40个字节,其内容要根据动态分配分配内存。需要注意的是,deque查找等动作,都需要首先检查指针是否已经在边界,进行索引表的步进,然后进行具体查找。
- 红黑树和散列表是关联式容器实现的关键。红黑树是一种平衡的二元搜寻树。不应该使用迭代器改变红黑树的元素值。因为红黑树内部有一定的排序规则。红黑树有多个参数,其中包括key,value,KeyOfValue,compare,alloc。红黑树内部有两种插入方式,包括insert_equal和insert_unique
二、容器实现深度解析
2.1 序列式容器
| 容器 | 底层结构 | 关键特性 |
|---|---|---|
| vector | 动态数组 | 三指针控制:start, finish, end_of_storage |
| deque | 分段连续+索引表 | 迭代器含cur, first, last, node指针 |
| list | 双向链表 | 节点含prev, next, data指针 |
vector内存增长示例:
1 | vector<int> v; |
2.2 关联式容器
1 | graph TD |
红黑树关键特性:
- 每个节点非红即黑
- 根节点必须为黑
- 红色节点的子节点必须为黑
- 任意节点到叶子的路径包含相同数量黑节点
三、迭代器设计哲学
3.1 迭代器核心接口
1 | template<class T> |
- 迭代器是一种泛化的指针。要注意迭代器内部操作符重载的写法,例如++操作符、*、&等重载的具体实现。另外以及iterator中的设计原则,iterator必须提供五种Type。这些参数在
<iterator>头文件中通过iterator结构体定义,并在自定义迭代器时需要用到。这五个参数按顺序分别是:
value_type: 迭代器所指向的元素的类型。通过迭代器,我们可以访问到容器中存储的元素,而value_type就定义了这些元素的类型。例如,对于std::vector<int>::iterator,其value_type就是int。difference_type: 用于表示迭代器之间距离的类型。通常情况下,这个类型是带符号的整型,例如std::ptrdiff_t。它可以用来计算两个迭代器之间的元素个数。例如,如果你有两个指向std::vector<int>中不同元素的迭代器it1和it2,那么it2 - it1的结果类型就是difference_type。pointer: 指向value_type的指针类型。通常情况下,它是value_type*。这个类型在某些迭代器(例如原始指针迭代器)中直接使用。reference: 指向value_type的引用类型。通常情况下,它是value_type&。当我们通过解引用迭代器(使用*运算符)访问元素时,得到的就是一个reference类型的对象。iterator_category: 描述迭代器所支持的操作的标签类型。STL 定义了五种主要的迭代器类别,它们之间存在着功能上的包含关系:std::input_iterator_tag: 只支持单向读取操作,即只能使用*it读取元素,并使用++it使迭代器前进。输入迭代器通常用于单次遍历的输入流。std::output_iterator_tag: 只支持单向写入操作,即只能使用*it = value写入元素,并使用++it使迭代器前进。输出迭代器通常用于单次遍历的输出流。std::forward_iterator_tag: 支持输入迭代器的所有操作,并且可以多次遍历容器中的元素。这意味着你可以保存一个前向迭代器的副本,并在之后再次使用它从相同的位置开始遍历。std::bidirectional_iterator_tag: 支持前向迭代器的所有操作,并且可以双向移动,即可以使用--it使迭代器后退。std::list、std::set和std::map等容器的迭代器通常是双向迭代器。std::random_access_iterator_tag: 支持双向迭代器的所有操作,并且提供随机访问的能力。这意味着你可以像操作数组指针一样,使用it + n、it - n、it[n]以及比较运算符(<、>、<=、>=)在常数时间内访问任意位置的元素。std::vector、std::deque和数组的迭代器都是随机访问迭代器。
3.2 迭代器分类与能力
| 迭代器类型 | 支持操作 | 典型容器 |
|---|---|---|
| 随机访问迭代器 | ++, --, +n, -n, [] |
vector, deque |
| 双向迭代器 | ++, -- |
list, set/map |
| 前向迭代器 | ++ |
forward_list |
| 输入/输出迭代器 | 单次遍历 | istream, ostream |
对于STL的容器等部件实际上是一个类模板,而算法实际上是一个函数的模板。另外函数一般会有多个重载方式,通过参数类型、数量等来区分。算法只会接收迭代器,看不到容器,所以它所需要的所有信息必须从迭代器中获得,而迭代器必须能够回答算法的所有问题。
四、算法与仿函数协作机制
4.1 算法模板示例
1 | template <class InputIterator, class T> |
仿函数(functors),仿函数是通过设计一种类通过重载小括号来近似实现函数的功能,是为算法进行服务的,分别有算术类,逻辑运算类,以及相对关系类。例如针对自定义的一种类,来定义一种独特的排序方式。
再STL中规定了每个Adaptor都应该挑选一个适配者继承,因为在函数应用到仿函数的时候很有可能会询问仿函数一些基础参数问题,因此需要像迭代器那样,给它一个继承的身份。
4.2 仿函数与适配器
算术仿函数示例:
1 | template <class T> |
适配器应用场景:
1 | // 将普通函数转换为仿函数 |
五、内存管理:分配器实现
5.1 GNU 2.9 allocator设计
1 | class __malloc_alloc_template { // 一级分配器 |
5.2 内存池工作流程
- 维护16个自由链表($8-128$字节)
- 内存不足时向系统申请大块内存
- 碎片回收通过自由链表管理
六、STL设计精髓总结
- 泛型编程思想:通过模板实现算法与数据类型的解耦
- 低耦合高内聚:容器、迭代器、算法通过标准接口协作
- 效率优先:通过内存池、红黑树等结构优化性能
- 可扩展性:允许用户自定义分配器、仿函数等组件