目录
STL包含六大组件,这些组件是杂糅到一起的,互相之间有关联。
问到就详细讲容器和迭代器。
1. 容器、迭代器、算法
上一篇讲了容器,这里主要介绍它和其他组件的关系
组件 | 与容器的关系 | 典型协作场景 |
---|---|---|
迭代器 | 容器提供迭代器接口 begin()、end() | 算法通过迭代器访问容器内容 |
算法 | 通过迭代器操作容器 | sort() , find() 等算法处理容器数据 |
仿函数 | 定义容器排序/比较规则 | set 的比较函数 |
适配器 | 基于容器构建特殊接口 | stack 基于deque 实现 |
分配器 | 控制容器内存管理 | 自定义内存分配策略 |
算法通过迭代器操作容器
同一算法可作用于不同容器
不同算法对迭代器类别有不同要求:
sort
需要随机访问迭代器(不能用于list
)find
只需要输入迭代器(可用于所有容器)
什么是迭代器?
迭代器的设计非常的天才,它充分体现面向对象的封装特性,容器的底层各不相同(哈希、树、数组等),容器内部是私有的,外部访问不了他们的内部结构,使用迭代器封装,在不需要暴露底层复杂结构的同时,还提供了统一、简单的方式访问容器,也就是它提供了一种统一的方法来访问容器中的元素,而不需要了解容器的内部结构。
迭代器的作用?
迭代器把容器和算法连接了起来,作为算法和容器之间的桥梁,算法通过迭代器直接访问各种容器,如果不符合算法要求又可以直接报错。比如sort算法,在设计时设计的是需要给他传一个随机迭代器,如果给他一个其他类型的迭代器则会报错。
算法的具体实现,直接脱离了具体的存储结构,也就是各种容器,直接通过迭代器来实现,依赖各种迭代器的类型,体现了设计角度的复用,使得同一算法可应用于任何满足迭代器要求的容器。
指针本身就是随机访问迭代器,使得传统数组也能参与算法:
int arr[10] = {...};
std::sort(arr, arr + 10);
迭代器的类型?
在C++标准库中,不同容器支持不同类型的迭代器:
- std::list:使用双向迭代器,支持前向和后向遍历。
- std::vector 和 std::deque、std::string:使用随机访问迭代器,支持任意位置的访问和高效的算术运算。
- std::forward_list:使用前向迭代器,仅支持单向遍历。
- std::set, std::map, std::multiset, std::multimap:使用双向迭代器。
- std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap:使用前向迭代器。
层级低的容器不能访问层级高的算法。层级高的容器可以访问层级低的函数。比如:vector和string就可以调用reverse(双向)。
按功能从弱到强排列如下:
- 输入迭代器(Input Iterator)
- 输出迭代器(Output Iterator)
- 前向迭代器(Forward Iterator)
- 双向迭代器(Bidirectional Iterator)
- 随机访问迭代器(Random Access Iterator)
比如说 sort 使用的就是随机迭代器, list 就不能调用它,因为 list 使用的是双向迭代器。
迭代器失效
迭代器失效是指一个原本指向容器元素的迭代器,在容器被修改后,可能变得无效。使用失效的迭代器会导致程序运行错误甚至崩溃。
对于 std::vector
来说,以下操作可能导致迭代器失效:
内存重新分配:
- 扩容时,底层存储数据的内存地址会重新分配。
- 所有现有的迭代器、指针和引用都会失效
- 元素插入或删除
- 插入或删除可能会导致部分迭代器失效,尤其是插入删除位置的迭代器
vector扩容,也就是说vector底层旧空间被释放掉,而在打印时,it还使用的是释放前的旧空间,在对it进行操作时,实际操作的是一块已经被释放的空间,从而引起崩溃。
erase删除某位置的元素后,pos位置之后的元素会向前搬移,没有导致底层空间的改变,理论上迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end的位置,而end位置是没有元素的,那么pos就失效了。因此删除vector中任意位置上元素时,vs就认为该位置迭代器失效。
其他:
string下的失效,与vector类似,string在插入+扩容操作+erase后,迭代器也会失效,不能再访问。erase函数执行后,it所指向的节点已经被删除,因此it无效,在下一次使用it时,必须先给其赋值。也就是用迭代器接受一下erase的返回值。
list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在被删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
迭代器的实现细节:
2. 适配器
什么是适配器?
适配器是一种结构型设计模式,它将一个类的接口转换成客户所期望的另一个接口。在STL中,适配器主要作用于容器和迭代器,通过封装现有组件并提供新的接口来满足不同的需求 。
主要和容器有关,也可能和迭代器有关,比如说反向迭代器就是一个适配器,适配器体现的是封装复用。所以可以分为容器适配器和迭代器适配器。
适配器种类:
容器适配器:10.stack 和 queue_queue 与stack-CSDN博客
迭代器适配器(以反向迭代器为例):10.stack 和 queue_queue 与stack-CSDN博客
正向序列: [A][B][C][D][E]
↑ ↑ ↑ ↑ ↑ ↑
正向迭代器:begin end
反向序列: [E][D][C][B][A]
↑ ↑ ↑ ↑ ↑ ↑
反向迭代器:rbegin rend
对应正向: end begin
rbegin()
的实现:内部实际上存储的是
end()
迭代器但解引用时返回的是
end()-1
位置的元素
rend()
的实现:内部实际上存储的是
begin()
迭代器解引用时会试图访问
begin()-1
(这是未定义行为)
对称性:
rbegin().base() == end()
rend().base() == begin()
3. 仿函数
什么是仿函数?
仿函数(Functor),也称为函数对象,是C++中一种特殊的类。它通过重载operator()操作符,使得该类的对象可以像普通函数一样被调用。
针对算法可以实现各种功能,最经典的就是比大小,特点就是可以控制规则
仿函数与算法和容器的关系:
算法通过仿函数参数实现灵活的行为控制:
// 排序算法使用仿函数定义比较规则
sort(vec.begin(), vec.end(), greater<int>());
某些容器依赖仿函数定义元素的组织方式:
// set使用less仿函数默认升序排列
std::set<int, std::less<int>> ascendingSet;
// priority_queue使用greater仿函数实现小顶堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
C++11后,lambda表达式成为创建仿函数的便捷方式:
sort(v.begin(), v.end(), [](auto& a, auto& b) {
return a.value < b.value;
});
4.空间配置器
空间配置器是STL中负责内存管理的核心组件,它为容器提供统一的内存分配和释放接口。
主要职责是:
内存的分配与释放
对象的构造与析构
提供标准化的内存管理接口
所有STL容器都使用空间配置器管理内存,默认使用std::allocator:
template <class T, class Allocator = std::allocator<T>>
class vector;
template <class T, class Allocator = std::allocator<T>>
class list;
其他功能:
解耦内存策略:将内存管理与容器实现分离
灵活扩展:允许自定义内存管理策略(如内存池、共享内存等)
5.STL的优缺点是什么?设计的好的地方和不好的地方?
STL缺点:
- 不支持线程安全(注意容器不是线程安全的)
- 没有持续更新,增加新容器(跳表。。。)
- 有一些冗余设计,如array,forward_list 等
STL优点:
1. 泛型编程(核心优势)
STL通过模板实现了类型无关的通用数据结构和算法,一套代码可处理任意数据类型,极大提高了代码复用率。
2. 高性能(核心竞争力)
采用编译时多态(模板)替代运行时多态(虚函数),算法经过深度优化(如sort用内省排序),内存管理可定制(如内存池),效率接近手写C代码。
3. 组件化设计(架构优势)
六大组件通过迭代器解耦,扩展性强且维护成本低。
- 空间配置器给容器提供统一的内存分配和释放接口
- 适配器作用于容器,通过封装现有组件并提供新的接口来满足不同的需求(stack、queue)
- 适配器作用于迭代器,通过封装现有组件并提供新的接口来满足不同的需求(反向迭代器)
- 容器提供了迭代器接口(begin、end)
- 迭代器用来实现了算法,算法脱离具体的存储结构
- 算法通过统一简单的方式:迭代器,操作各种底层不同容器
- 迭代器作为算法和容器之间的桥梁存在
- 仿函数给容器提供了元素组织方式(默认建大堆→建小堆)
- 仿函数给算法提供了更灵活的机制(sort默认排升序→排降序)