持续更新中…
零.组成部分
STL标准库中共有六大组成部分:分配器,容器,迭代器,算法,仿函数,适配器。平时我们熟知的一般是容器,算法,迭代器。
- 分配器是STL中用于分配内存的部分。
- 容器是STL定义好的,可以让我们直接使用的模板容器。
- 迭代器是STL为我们提供的访问容器的一种方式,同时也是算法和容器的粘合剂,让算法和容器结合起来。
- 算法是一系列比较具有通用性的模板函数,通过迭代器作用在容器上。
- 仿函数是类重载了(),具有函数功能的类
- 适配器分为多种,容器适配器是指队列和栈,算法适配器可以为函数绑定参数,类似bind,还有迭代器适配器,如插入适配器。
STL中大量使用了模板编程,模板编程中有一种使用方式叫做特化,而特化分为三种,不特化,偏特化,全特化。
- 不特化是指,所有模板参数都没有被重写成特定类型;
- 偏特化是指,有一部分模板参数被重写成特定类型;
- 特化是指,所有模板参数都被重写成特定类型;
特化让模板编程即可以提供模板化的编程,也可以不同参数属性进行特殊处理。
一.分配器
STL中用于分配内存的部件,内部分成两级分配器。
- 当容器申请的内存大于128字节时,调用第一级分配器,使用使用全局的
operator new
和operator delete
函数来分配和释放内存进行分配内存。 - 当申请的内存小于128字节时,则使用第二级分配器,使用内存池的方式分配内存。
第二种分配方式,也体现了内存池的作用,在小块内存的申请释放中具有优势,同时减小了一级分配器的申请大片空间后内部碎片的问题。
STL中内存池的机制为,他将128字节,分为16份,每份为8的倍数,保存为一个数组,数组每个元素是一个链表,链表上串着预先申请好的空间,第一个元素就对应链表每个节点对应8个字节的空间,第二个元素就对应16个字节空间,最后一个元素就对应128字节空间。
当有申请到来时,先将所需要的空间大小扩大为8的倍数,然后在对应链表上找一片未分配的空间分配。
这时候若是对应链表上不存在空间,链表就会执行申请内存函数,这个函数中保存着一大片预先申请好的内存,直接申请20个对应空间大小的空间,返回给链表,链表其中一个空间返回给申请者,其余串在自己的链表中。
若是不足20个,就有几个分配几个,若是满足一个的空间都没有,就会再去malloc申请一大片空间。
二.容器
容器又分为序列式容器和关联式容器
1.序列式容器
序列式容器主要为三个:vector
,list
,deque
(1).vector
vector 是一种可变长度的数组,相比于普通数组加入了可以扩容的特性。
vector在使用时会有两个重要的属性,size(大小)和capital(容量)
- size:表示容器中已经保存的元素的数量。
- capacity:表示容器中的内存大小。
size是指容器中元素的个数,容量是内存空间大小。换句话说,size是箱子里放了多少东西,capacity是箱子有多大。
扩容机制
在vector
插入过程中,如果容量不如,vector
会进行扩容,扩容大小根据编译器不同,为1.5倍或者两倍。
扩容的机制为,从新申请一份两倍当前容量的空间,将现在的元素都拷贝过去,以为发生了位置的移动,所以插入后的vector
迭代器会失效。
大量数据插入
由于扩容的机制,我们可以知道,频繁的扩容会带来很大的开销(每次扩容都需要全部复制),所以对于大量数据的插入,我们需要通过vector
下的reserve
函数,先手动去对容量扩容,再去插入元素。
大量数据的缩减容量问题
假设插入了100亿数据,空间可能变成了200亿,这样有100亿的空间我们可能根本用不上,就浪费了。
这里有两种解决方法:
- 使用
swap
具体方法为,我们创建一个空的vector
,然后掉用swap
函数与原数组交换两次,这样原数组的容量就呗缩减到元素个数了。 - 使用
shrink_to_fit
直接调用这个函数就能直接将容量调整到size的大小。
push_back
和 emplace_back
的区别
push_back
在插入数据时,会将数据copy一份,在把拷贝后的元素放到vector
后面emplace_back
在插入数据时,不会将数据copy,直接将数据放到vector
后面,对于自定义类型通过移动拷贝构造实现的
(2).list
容器底层
list
是stl中的双向链表,内部为环形链表,保存的的链表的头节点同时也是链表的尾节点,list
因为节点是通过指针一个一个串起来的,所以list
的空间是不连续的。
迭代器
list中的迭代器是双向迭代器,因为它的空间不是连续的所以list
的迭代器不是random的。
list
内置的sort排序
由于sort中使用的迭代器需要是random的,所以list能使用sort进行排序,但是list
类自己实现了一个sort排序,使用的是归并排序,通过.sort()
直接调用。
关于list的拷贝
list本身的拷贝是深拷贝,但是他内部的元素是浅拷贝
(3).deque
底层实现
deque
的底层是由一段一段等长的连续空间组成的,类似一个二维数组,所以deque是部分连续,初始位置在中间的那段空间,当某个方向上的空间用完时,就再加上一段空间,可以满足前后动态增长。
deque
的增长方式和vector
区别很大,deque
是空间不足了,用一段新的空间补在不足的位置,原来的数据没有改变,vector
的空间不足他会重新申请一片更大的空间,把所有的元素拷贝到新的空间上面去。所以deque
在扩容时的开销更低。
迭代器
deque
的迭代器是random迭代器,支持随机访问,但是效率不如vecotr,每次访问需要计算在哪片连续空间上,逻辑上是连续的。
因为迭代器是random迭代器,所以deque是可以使用sort进行排序的。
使用vector
,list
,deque
的时机
vector
适用于需要随机访问的场合,随机访问时O(1),有序数组的查询可以使用二分查找就是O(log n),插入和删除都是O(n)list
适用于需要频繁插入删除的场合,他的插入删除都是O(1),但是不支持随机访问,查询正常情况下是O(n),但是有序链表可以通过跳表数据结构降为O(log n)deque
适用于两边都与要动态扩展的场合,deque
也支持随机存储,但是deque
的随机存储相对于vector
慢,插入删除查询都是O(n);
(4).容器适配器 queue
queue
是通过对deque
的封装实现的,满足适配器模式。他只保留了deque
的向后增长。
(5).容器适配器 stack
stack
是通过对deque
的封装实现的,满足适配器模式。他只保留了deque
的向前增长。
2.关联式容器
关联式容器中使用了两种数据结构,map
和set
使用的式红黑树,而priority_queue
使用的是堆。
- 红黑树:是一种自平衡的二叉树,通过满足红黑树的条件,达到一个几乎平衡的二叉树。
- 堆:分为大顶堆和小顶堆,大顶堆就是他的根节点比他的叶子节点大,小顶堆就是根节点比他的叶子节点小。
(1).set
set
的底层实现是红黑树,并且不能重复,红黑树会按照set
的键排序,set
的键和值是同一个,所以set是有序的。
set
的插入使用insert_into()
对于元素的插入和查询都是log(n)的时间复杂度。
set
通常可以用于去重操作。
(2).map
map
的底层也是使用的红黑树,可以存储键值对,通过键值可以在log(n)的时间找到对应的值。map
的键也是有序的
map的插入直接通过[], ma[key] = val;
- 若是map中不存在key则插入key,对应值为val
- 若map中存在key则将key对应的键值修改为val
map
访问有三种方式:
- 通过重载的[]可以向访问数组一样访问map中的值,
ma[key] = val
- 通过迭代器进行访问 ,
auto ite = ma.begin(); ite->first , ite->second
- 通过函数at访问,
auto val = ma.at(key)
(3).priority_queue
priority_queue
的底层采用的是大顶堆,他保证队首元素是队列元素的最高的值,即最大的值。
3.hash容器
使用hash表时,键可以通过哈希函数计算出哈希值,通过哈希值可以在o(1)的时间复杂度找到对应的值。
hash表在设计的时候需要考虑hash冲突的情况,解决哈希冲突共有三种常用方法
- 一次探测
- 二次探测
- 开链
(1).unorder_set
特点
无序存储:unordered_set 不保证元素的顺序。它将元素分布在桶(buckets)中,元素的顺序取决于其哈希值。
唯一性:unordered_set 中不允许有重复的元素。如果你尝试插入已经存在的元素,插入操作会失败。
快速操作:插入、删除和查找的平均时间复杂度为 O(1)(哈希表的优势)。但是,在最坏情况下,这些操作的时间复杂度可能为 O(n),当哈希函数不理想时会发生哈希冲突。
哈希函数:unordered_set 依赖于哈希函数来确定元素的位置。默认情况下使用标准的 std::hash 函数对象,可以根据需要提供自定义哈希函数。
不支持随机访问:由于元素是无序的,unordered_set 不支持通过索引访问元素。
使用方法
unordered_set
适合用于需要快速查找元素的场景,例如用于去重、集合运算以及存储大量无序且需要频繁查找的元素集合。
主要使用方法为插入和查找,由于不支持随机访问,unordered_set
通过insert
和find
进行操作
- 插入:insert() 用于插入元素。
- 查找:find() 返回给定元素的迭代器,如果元素不存在则返回 end()。
与set
区别
- unordered_set 使用哈希表,操作更快,但元素无序;
- set 使用平衡二叉搜索树,元素有序,支持有序遍历,但操作的时间复杂度为
O(log n)。
(2).unorder_map
特点
unorder_map与unorder_set特点基本相同,区别是unorder_map存储的是键值对。
使用方法
unordered_map
非常适合需要根据键快速查找值的场景,比如字典、计数器、缓存等。这种容器提供了快速查找功能,尤其在处理大量数据时效率较高。
unorder_map
使用方式与map
相同,也可以通过三种方式访问:
operator[]
或at()
可以用来根据键访问对应的值。find()
返回给定键的迭代器,如果键不存在则返回end()
。
与map
区别
unordered_map
使用哈希表实现,因此键值对是无序的,插入、查找和删除的平均时间复杂度为 O(1)。map
使用平衡二叉树(通常是红黑树)实现,键值对是按键的顺序存储的,所有操作的时间复杂度为 O(log n)。
常见问题
扩容时机
默认情况下,C++ 的 unordered_map
会在负载因子达到 1.0(即元素数量与桶的数量相等)时触发扩容。可以通过 max_load_factor()
设置不同的负载因子阈值。
迭代器失效
迭代器失效的情况发生在一个容器扩容,地址改变,目标元素删除等情况。
对于unordered_map
来讲,主要有以下几种情况会导致迭代器失效:
负载因子达到阈值触发扩容,或者交换空间,清空容器,此时所有迭代器失效,容易理解,所有的元素空间都变了
删除元素时,因为unordered_map
只有在负载变高时会自动扩容,不会自动缩容,所以删除操作只会导致这个元素失效。
三.迭代器
迭代器提供了一种统一的方式来遍历和操作容器中的元素。 迭代器就像指针一样,可以用来访问容器中的元素,同时隐藏容器内部的具体实现细节,使得代码更加通用和灵活。
迭代器的作用
迭代器类似于指针,但它比指针更强大和灵活。迭代器将容器的底层结构(如数组、链表、树等)与访问方式分离,使得用户可以通过相同的接口操作不同类型的容器,而不用关心容器的具体实现细节。
常见的 STL 容器如 vector、list、map、unordered_map 等都有迭代器,它们都支持通过迭代器遍历元素。
迭代器的原理
迭代器通过类模板的方式实现,通过符号重载具有类似指针的行为,但提供了更多功能和更好的抽象。
迭代器的种类
迭代器主要包含5种:
- 输入迭代器(只能单向读取,不能写)
- 输出迭代器(只能单向写,不能读取)
- 前向迭代器(只能前向读写,不能后退)
- 双向迭代器(能双向读写)
- 随机访问迭代器(支持双向读写,和数组一样的随机访问)
常见容器与迭代器对应关系如下表所示:
容器类型 | 支持的迭代器类型 | 描述 |
---|---|---|
vector | 随机访问迭代器(Random Access Iterator) | vector 是动态数组,支持随机访问,因此它的迭代器支持所有操作。 |
deque | 随机访问迭代器(Random Access Iterator) | 双端队列,支持随机访问,迭代器与 vector 类似。 |
list | 双向迭代器(Bidirectional Iterator) | list 是双向链表,只支持双向迭代,不支持随机访问。 |
forward_list | 前向迭代器(Forward Iterator) | 单向链表,只支持单向遍历。 |
set / multiset | 双向迭代器(Bidirectional Iterator) | 基于平衡树实现,支持双向遍历。 |
map / multimap | 双向迭代器(Bidirectional Iterator) | 基于平衡树实现,支持双向遍历。 |
unordered_set / unordered_map | 前向迭代器(Forward Iterator) | 基于哈希表实现,只支持单向遍历。 |
stack / queue | 不支持迭代器 | stack 和 queue 是抽象的容器适配器,无法遍历元素。 |
array | 随机访问迭代器(Random Access Iterator) | 固定大小的数组容器,支持随机访问。 |
常见问题
迭代器失效(Iterator Invalidation)
迭代器失效是指当容器结构发生变化后,之前获得的迭代器可能不再有效,访问这些迭代器会导致未定义行为。迭代器失效通常发生在以下操作中:
- 插入元素后迭代器失效
- vector 和 deque:如果在 vector 或 deque 中插入新元素,可能会导致底层内存重新分配,导致所有迭代器失效。
- list:在 list 中插入元素不会导致迭代器失效,因为 list 是链表,插入只影响局部指针。
- unordered_map 和 unordered_set:插入新元素可能会触发哈希表的重哈希,导致所有迭代器失效。
- 删除元素后迭代器失效
- vector 和 deque:删除元素会导致被删除元素之后的所有迭代器失效,因为元素要向前移动以填补删除的位置。
- list:删除当前节点只会使指向被删除节点的迭代器失效,其他迭代器不受影响。
- unordered_map 和 unordered_set:删除操作不会影响其他迭代器,只有指向被删除元素的迭代器会失效。
- 通过 std::move 或 std::swap 交换空间后得迭代器