C++STL
STL概述
Standard Template Library,标准模板库,是C++的标准库之一,一套基于模板的容器类库,还包括许多常用的算法,提高了程序开发效率和复用性。STL包含6大部件:容器、迭代器、算法、仿函数、适配器和空间配置器。
-
空间配置器
负责容器空间配置和管理。具体包括容器内对象的动态内存分配和释放及对象的构造和析构。
-
迭代器
扮演容器与算法之间的胶合剂,是所谓的“泛型指针”。从实现角度看,迭代器是一种将operator*,operator->,operator++,operator--等指针相关操作予以重载的class template。所有STL容器都附带有自己专属的迭代器,只有容器设计者才知道如何设计迭代器。
-
容器
各种数据结构的类模板。
-
仿函数(函数对象)
仿函数是一种重载了operator()来模仿函数的类模板。相比于函数指针具有更好的抽象。
-
算法
各种常见算法的函数模板。
-
适配器
对容器、仿函数、迭代器、进行修饰来适应特点场景。
空间配置器
根据STL标准规范,空间配置器STL allocator需要实现以下接口
-
对象构造由 ::construct() 负责;对象析构由 ::destroy() 负责。
-
内存配置由 alloc::allocate() 负责;内存释放由 alloc::deallocate() 负责;
SGI STL专属空间配置器alloc
STL标准是C++标准里面规定的模板库接口规范,但它只是接口规范,没有规定具体怎么实现,以SGI STL 为例,SGI STL 的空间配置器与众不同,且与 STL 标准规范不同。其名为 alloc,而非 allocator。虽然 SGI 也配置了 allocatalor,但是它自己并不使用,也不建议我们使用,原因是效率比较感人,因为它只是在基层进行配置/释放空间而已,而且不接受任何参数。
alloc具有两种配置器
根据 __USE_MALLOC的定义来确定使用哪种配置器

第一级配置器
第一级直接使用allocate()调用malloc()、deallocate()调用free(),使用类似new_handler机制解决内存不足(抛出异常),配置无法满足的问题(如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回NULL 指针,宣告内存申请失败)。

第二级配置器
第二级视情况使用不同的策略,当配置区块大于128bytes时,调用第一级配置器,当配置区块小于128bytes时,采用内存池的整理方式:配置器维护16个(128/8)自由链表,负责16种小型区块的此配置能力。内存池以malloc配置而得,如果内存不足转第一级配置器处理。


空间配置器存在的问题
- 自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费。
- 由于配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。释放时机就是程序结束,这样子会导致自由链表一直占用内存,自己进程可以用,其他进程却用不了。
容器
容器包含关联容器和顺序容器两大类。关联容器和顺序容器的根本不同在于:关联容器中的元素是按关键字来保存和访问的,而顺序容器中的元素则是按它们在容器中的位置来顺序保存和访问的。
vector
底层原理
vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
内存增长机制
当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。
容器容量capacity和大小size
容器内存空间是容量,包含元素个数是大小,当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
vector中的reserve和resize的区别
reserve容器预留的容量,但是此时容器内并无元素。resize是改变容器的大小,并且创建对象,第一个参数为容器的大小size,第二个参数即为新元素的值。
迭代器失效(判断原迭代器指向的元素的内存空间/地址是否发生改变)
因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。
- 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
- 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。
list
底层原理
list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。list不支持随机存取,如果需要大量的插入和删除较适合。
deque
底层原理
deque系由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。
deque采用一块所谓的map数组作为主控。这里所谓map是一小块连续空间,其中每个节点都是指针,指向一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。

内存空间增长
可以看到容量不够,就会重新配置新的map数组,拷贝原来数组中的指针,释放原来的空间。
提供随机访问
虽然内存空间只是分段连续的,但是每段大小相同,可以通过简单计算出指定索引的的内存地址。
priority_queue
底层原理
优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
map 、set、multiset、multimap
底层原理
map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树
红黑树的特性:
-
每个结点或是红色或是黑色;
-
根结点是黑色;
-
每个叶结点是黑的;
-
如果一个结点是红的,则它的两个儿子均是黑色;
-
每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。
判断key值
对于STL里的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0统计的是key出现的次数,因此只能为0/1,而mp.find(key) != mp.end()则表示key存在。
增删改查
- set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
- map和set的增删改查速度为都是logn,是比较高效的。
迭代失效情况(判断原迭代器指向的元素的内存空间/地址是否发生改变)
每次insert之后,以前保存的iterator不会失效,因为单个结点的增加,并不会改变其他节点内存分布。
unordered_map、unordered_set
底层原理
unordered_map的底层是一个防冗余的哈希表(采用除留余数法)。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。
但是,不能够保证每个元素的key与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。 一般可采用拉链法解决冲突:
可以设计一个函数(哈希函数(一般使用除留取余法),也叫做散列函数),使得每个元素的key都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照key为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶,每个桶都是单向链表.。

容器之间的比较
什么情况下用vector,什么情况下用list,什么情况下用deque?
-
vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
-
list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
-
需要从首尾两端进行插入或删除操作的时候需要选择deque。
unordered_map 与map的区别?使用场景?
构造函数:unordered_map 需要hash函数,等于函数;map只需要比较函数(小于函数).
存储结构:unordered_map 采用hash表存储,map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。
在数据量是一定数量级时,unordered_map 的查找效率会更高,但当数据范围很大时,unordered_map的内存使用效率更好。
迭代器
迭代器是连接容器和算法的一种重要桥梁。
- 包含了容器的信息,比如五种关联类型,迭代器类型,容器元素值类型,指针类型、距离类型、元素引用类型。算法可以依据这些信息,做特定处理。
- 迭代器是泛化的指针,每个容器针对自身特性重载设计了指针的操作例如++,--,计算距离distance,使得算法不了解容器内部原理的情况下遍历容器。
迭代器种类
-
输入迭代器:是只读迭代器,只支持自增,因此在每个被遍历的位置上只能读取一次。
-
输出迭代器:是只写迭代器,只支持自增,因此在每个被遍历的位置上只能被写一次。
-
前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–-,所以只能向前移动。
容器forward_list是单向链表,容器unordered_set、unordered_map、unordered_multiset、unordered_map哈希表中的每个桶都是单向链表.因此其迭代器只能单向移动,因此是forward_iterator类型。
-
双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。
容器list是双向链表,容器set、map、multiset、multimap本身是有序的,他们的迭代器都可以双向移动(++和—通过中序遍历实现),因此是bidirectional_iterator类型.
-
随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。支持前面四种Iterator的所有操作,并另外支持it + n、it - n、it += n、 it -= n、it1 - it2和it[n]等操作。
容器array、vector、deque对使用者来说是连续空间,是可以跳跃的,其迭代器是random_access_iterator类型.
迭代器失效的情况
当迭代器指向元素的内存地址发生变化,那么该迭代器就失效了。
兼容一般指针-萃取器
一些泛型算法怎么兼容普通指针类型?
迭代器的5个关联类型在容器类中均有定义,但是指针类型的关联类型需要根据指针类别进行确定,为了使STL算法同时兼容迭代器和一般指针,就在迭代器(指针)和算法之间加一个迭代器萃取器(traits).
-
萃取器实现
在实现上, iterator_traits 类使用模板的偏特化,对于一般的迭代器类型,直接取迭代器内部定义的关联类型;对于指针和常量指针进行偏特化,指定关联类型的值.
![image-20220716141906607]()


浙公网安备 33010602011771号