c++STL简介

持续更新中…

零.组成部分

STL标准库中共有六大组成部分:分配器,容器,迭代器,算法,仿函数,适配器。平时我们熟知的一般是容器,算法,迭代器。

  • 分配器是STL中用于分配内存的部分。
  • 容器是STL定义好的,可以让我们直接使用的模板容器。
  • 迭代器是STL为我们提供的访问容器的一种方式,同时也是算法和容器的粘合剂,让算法和容器结合起来。
  • 算法是一系列比较具有通用性的模板函数,通过迭代器作用在容器上。
  • 仿函数是类重载了(),具有函数功能的类
  • 适配器分为多种,容器适配器是指队列和栈,算法适配器可以为函数绑定参数,类似bind,还有迭代器适配器,如插入适配器。

STL中大量使用了模板编程,模板编程中有一种使用方式叫做特化,而特化分为三种,不特化,偏特化,全特化。

  • 不特化是指,所有模板参数都没有被重写成特定类型;
  • 偏特化是指,有一部分模板参数被重写成特定类型;
  • 特化是指,所有模板参数都被重写成特定类型;

特化让模板编程即可以提供模板化的编程,也可以不同参数属性进行特殊处理。

一.分配器

STL中用于分配内存的部件,内部分成两级分配器。

  • 当容器申请的内存大于128字节时,调用第一级分配器,使用使用全局的operator newoperator delete函数来分配和释放内存进行分配内存。
  • 当申请的内存小于128字节时,则使用第二级分配器,使用内存池的方式分配内存。

第二种分配方式,也体现了内存池的作用,在小块内存的申请释放中具有优势,同时减小了一级分配器的申请大片空间后内部碎片的问题。

STL中内存池的机制为,他将128字节,分为16份,每份为8的倍数,保存为一个数组,数组每个元素是一个链表,链表上串着预先申请好的空间,第一个元素就对应链表每个节点对应8个字节的空间,第二个元素就对应16个字节空间,最后一个元素就对应128字节空间。

当有申请到来时,先将所需要的空间大小扩大为8的倍数,然后在对应链表上找一片未分配的空间分配。

这时候若是对应链表上不存在空间,链表就会执行申请内存函数,这个函数中保存着一大片预先申请好的内存,直接申请20个对应空间大小的空间,返回给链表,链表其中一个空间返回给申请者,其余串在自己的链表中。

若是不足20个,就有几个分配几个,若是满足一个的空间都没有,就会再去malloc申请一大片空间。

二.容器

容器又分为序列式容器和关联式容器

1.序列式容器

序列式容器主要为三个:vectorlistdeque

(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_backemplace_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.关联式容器

关联式容器中使用了两种数据结构,mapset使用的式红黑树,而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通过insertfind进行操作

  • 插入: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)

迭代器失效是指当容器结构发生变化后,之前获得的迭代器可能不再有效,访问这些迭代器会导致未定义行为。迭代器失效通常发生在以下操作中:

  1. 插入元素后迭代器失效
  • vector 和 deque:如果在 vector 或 deque 中插入新元素,可能会导致底层内存重新分配,导致所有迭代器失效。
  • list:在 list 中插入元素不会导致迭代器失效,因为 list 是链表,插入只影响局部指针。
  • unordered_map 和 unordered_set:插入新元素可能会触发哈希表的重哈希,导致所有迭代器失效。
  1. 删除元素后迭代器失效
  • vector 和 deque:删除元素会导致被删除元素之后的所有迭代器失效,因为元素要向前移动以填补删除的位置。
  • list:删除当前节点只会使指向被删除节点的迭代器失效,其他迭代器不受影响。
  • unordered_map 和 unordered_set:删除操作不会影响其他迭代器,只有指向被删除元素的迭代器会失效。
  1. 通过 std::move 或 std::swap 交换空间后得迭代器

四.算法

五.仿函数

六.适配器

1.容器适配器

2.函数适配器

3.迭代器适配器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值