STL源码剖析

空间配置器 allocator

特殊的空间配置器SGI::alloc(2.9)

why采用这种?

我们都知道new的底层是调用malloc,但是malloc会分配大量的额外开销,这个开销是记录每个元素的大小的。这个开销其实是没有必要的,因为在容器中很多时候都是存放同一种类型的数据,其元素大小一致。

所以就有了这张图片
在这里插入图片描述
这里每个数字代表一种大小(比如#0就代表8个字节)。如果分配容器元素大小是8个字节(不是的话,会分配成8的倍数),那么就利用这个单向链表去管理,链表的每一块就是一个元素大小,就不会带有额外开销,整个链表只会带有一个记录大小的开销

SGI::allocator(4.9)

到了4.9版本,改了名字,但是2.9版本的优点继承了下来,名字是_pool_alloc

到了4.9版本,allcator的操作变成构造对象(new),和析构对象(delete)

  • 构造对象 new的操作,1.分配空间 2.构造对象的同时还要初始化
  • 析构对象 delete 1. 析构对象 2.空间释放

构造与析构

构造就是分配空间,然后调用构造函数并初始化
析构的话,这样需要注意

首先利用value_type()获取所指对象的型别,再利用__type_traits判断该型别的析构函数是否trivial(无关痛痒的系统默认的析构函数),若是(__true_type),则什么也不做,若为(__false_type),则去调用destory()函数。

也就是说,在实际的应用当中,STL库提供了相关的判断方法**__type_traits**,感兴趣的读者可以自行查阅使用方式。除了trivial destructor,还有trivial construct、trivial copy construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。

一级空间配置器

malloc_alloc_template
一级空间配置器中重要的函数就是allocate、deallocate、reallocate 。 一级空间配置器是以malloc(),free(),realloc()等C函数执行实际的内存配置 。大致过程是:

1、直接allocate分配内存,其实就是malloc来分配内存,成功则直接返回,失败就调用处理函数

2、如果用户自定义了内存分配失败的处理函数就调用,没有的话就返回异常

3、如果自定义了处理函数就进行处理,完事再继续分配试试

二级空间配置器 流程

default_alloc_template 默认的空间配置器,小于128字节使用。

这个就是上面提到到链表那种形式去管理分配的内存空间。管理链表的指针是一个共用体,因此只占用一个内存空间,不会存在一个节点一个指针的情况,节省内存。

1、维护16条链表,分别是0-15号链表,最小8字节,以8字节逐渐递增,最大128字节,你传入一个字节参数,表示你需要多大的内存,会自动帮你校对到第几号链表(如需要13bytes空间,我们会给它分配16bytes大小),在找到第n个链表后查看链表是否为空,如果不为空则标记result指针指向节点,然后把对应的free_list中拔出并返回,并将已经拨出的指针向后移动一位(代表头指针向后移动)。

2、对应的free_list为空,先看其内存池是不是空时,如果内存池不为空:
(1)先检验内存池剩余空间是否够20个节点大小(即所需内存大小(提升后) * 20),若足够则直接从内存池中拿出20个节点大小空间,将其中一个分配给用户使用,另外19个当作自由链表中的区块挂在相应的free_list下,这样下次再有相同大小的内存需求时,可直接拨出。

(2)如果不够20个节点大小,则看它是否能满足1个节点大小,如果够的话则直接拿出一个分配给用户,然后从剩余的空间中分配尽可能多的节点挂在相应的free_list中。

(3)如果连一个节点内存都不能满足的话,则将内存池中剩余的空间挂在相应的free_list中(找到相应的free_list),然后再给内存池申请内存,转到3。

3、此时内存池为空,要申请内存 此时二级空间配置器会使用malloc()从heap上申请内存,(一次所申请的内存大小=2 * 所需节点内存大小(提升后)* 20 + 一段额外空间),申请40块,一半拿来用放在free_list(1个交出,剩下19个给free_list维护),一半(20个)放内存池中。

4、malloc没有成功 在第三种情况下,如果malloc()失败了,说明heap上没有足够空间分配给我们了,这时,二级空间配置器会从比所需节点空间大的free_list中一一搜索,从比它所需节点空间大的free_list中拔除一个节点来使用。如果这也没找到,说明比其大的free_list中都没有自由区块了,那就要调用一级适配器了。

释放时调用deallocate()函数,若释放的n>128,则调用一级空间配置器(见上一个步骤),否则就直接将内存块挂上自由链表的合适位置。

扩容问题
看阿秀总结的流程图片

二级空间配置器的问题

STL二级空间配置器虽然解决了外部碎片与提高了效率,但它同时增加了一些缺点:

1.因为自由链表的管理问题,它会把我们需求的内存块自动提升为8的倍数,这时若你需要1个字节,它会给你8个字节,即浪费了7个字节,所以它又引入了内部碎片的问题,若相似情况出现很多次,就会造成很多内部碎片;

2.二级空间配置器是在堆上申请大块的狭义内存池,然后用自由链表管理,供现在使用,在程序执行过程中,它将申请的内存一块一块都挂在自由链表上,即不会还给操作系统,并且它的实现中所有成员全是静态的,所以它申请的所有内存只有在进程结束才会释放内存,还给操作系统,由此带来的问题有:1.即我不断的开辟小块内存,最后整个堆上的空间都被挂在自由链表上,若我想开辟大块内存就会失败;2.若自由链表上挂很多内存块没有被使用,当前进程又占着内存不释放,这时别的进程在堆上申请不到空间,也不可以使用当前进程的空闲内存,由此就会引发多种问题。

迭代器概念与traits编程

迭代器概念

-迭代器是一种行为类似指针的对象,用于连接容器和算法。实现对于一个容器不必知道它的类型,直接获得它的迭代器就可以用于算法的执行。因此,在迭代器要传递所指对象类型或者可以获取到对象的类型。

traits技巧

在这里插入图片描述

  • 萃取的意义就在于可以针对不同的class提取出不同的迭代器(需要针对类型设计迭代器),从而提高方便对容器进行操作。

  • 每个迭代器都包括以下5个东西:value_type, difference type, pointer, reference, iterator categoly。为了使容器可以融入STL中,一定要为容器的迭代器定义着五种相应类型。“特性萃取机”traits会把这些特性萃取出来。

  • iterator categoly代表迭代器的种类,有5种 inputiterator/ outputiterator /Forwarditerator /Bidirectional iterator/ Random Access iterator

  • 另外萃取也是通过模板偏特化的方式,实现对普通指针,const指针的支持

简单来说:迭代器就是告诉算法,要使用的容器的类型(所以迭代器是算法和容器的桥梁)

在这里插入图片描述

序列式容器

1.vector

动态增长机制:GCC以2的指数速度增长,vs以1.5倍的速度增长。考点!为什么是2倍?1.可以减少以后扩容复制的次数 2.平均到每个元素可以实现O(1)的时间复杂度, n+n/2+n/4…=n
具体参考:vector扩容为什么是2倍数或者1.5倍数

而在不断pop的过程中,无论原先分配的空间有多大,都不会动态减少。

迭代器特性:可以以普通指针做迭代器。一旦引起空间重新配置,指向vector的所有迭代器就都失效了。

vector的数据结构采用线性连续空间,通过两个迭代器start, finish分别指向配置空间中已被使用的范围,end_of_storage指向整块连续空间的尾端。

一个vector的容量永远大于等于其大小,当vector空间不够用时,容器的扩张必须经过**“1重新配置空间、2元素移动、3释放原空间“**等过程。扩充空间的事件成本比较高,为避免多次扩充,我们会将容量扩充两倍。如果两倍还不够用,就扩充更大的容量。(因此不能分配前一个空间)

2.List

List基于双向链表实现

List的插入、删除或者拼合操作不会造成原有迭代器的失效,每次插入都是在位置前插入。

List不能用STL 中的sort函数进行排序,而是要用自身的sort函数。List仅支持随机访问迭代器,而List是双向迭代器。

3.deque

是一个双向队列

将数据分为多个存储区域,存储区域的指针存储在map存储区中,当map区域不够大时可以动态增长。所以deque可以在常数时间在首尾进行存取操作,也支持随机访问。

map的扩容是两倍,并且扩容以后会复制会中间,而不是从头复制 这样才可以做到双端
在这里插入图片描述

stack

stack是一种先进后出的数据结构。stack只允许新增元素、移除元素、取得栈顶元素。stack以deque为底部结构并封闭其头端开口,便轻而易举形成一个stack。SGI STL默认以deque作为stack底部结构stack是一个适配器, stack不允许有任何遍历行为。(stack没有迭代器)

queue

queue是一种先进先出的数据结构。它有两个出口,形式如图所示。stack允许新增元素、从底端移除元素、取得最顶元素。但除了底端可以加入,最顶端可以取出外,没有其他任何方法可以存取queue的其他元素。换言之,queue不允许有任何遍历行为。(queue没有迭代器)

heap

采用以vector保存的完全二叉,并可通过sift_up和sift_down进行堆调整。

priority_queue

priority_queue有一个优先级的概念,默认采用max-heap。它是利用一个make_heap完成,而heap又是以vector呈现。

关联式容器

基本概念:
1、二叉搜索树
任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。其增删改查都很简单。

2、AVL-tree
在二叉搜索树的基础上,要求任何节点的左右子树高度相差最多1

当插入新节点时可能会改变平衡状态,此时需要重新调整。只需要调整“插入点指根节点”路径上,平衡状态被破坏之各节点中最深的哪一个,假设此点为X,分为四种情况:

插入点位于X的左子节点的左子树–左左
插入点位于X的左子节点的右子树–左右
插入点位于X的右子节点的左子树–右左
插入点位于X的右子节点的右子树–右右
对于1,4情况,可以采用单旋转操作调整解决,对于2,3情况,可以采用双旋转操作调整解决。

3、红黑树
红黑树在二叉搜索树的基础上需要满足以下规则:

每个节点不是黑色就是红色
根节点为黑色
如果节点为红色,其子节点必须为黑色
任一节点至NULL的任何路径,所含之黑节点数必须相同。

set

set的特性是所有元素的键值自动被排序,set的不允许有两个相同的键值,其中的元素不能被改变,以RB-tree为底层机制

红黑树特点?

  • 红黑树的平衡性是比AVL-tree弱的,但是搜索效率几乎相等。两者的插入和删除操作都是O(logn),但是就旋转操作而言,AVL-tree是O(n),而红黑树是O(1)

map

map的特性是所有元素都会根据元素的键值自动被排序。map的元素由pair组成,同时拥有key和value。不允许由相同的key。

multiset/multimap于set/map唯一差别在于插入时使用insert_equal()

hashtable

使用hash function将元素映射的某一位置上,但这无法避免的会产生碰撞解决方法有线性探测、二次探测、开链等。

  • 线性探测

使用hash function计算出某个元素的插入位置,如何该位置上的空间不可用时,继续向下寻找可用空间。

存在的问题:可能过去的元素集中在某一区域,导致需要不断的解决碰撞问题。

  • 二次探测

采用F(i)=i^2映射函数,当发生碰撞时,每次向下寻找第1、4、9、16…位置上的空间是否可用

  • 开链

每一个表格元素维护一个list,hash function会分配某一个list

unordered_map/ unordered_set

vector+list作为底层,vector存放桶子,每个桶子挂一个单向链表(每次碰撞,桶子的链表增加)
当元素的个数全部加起来大于桶子的个数就要扩容了,扩容采用vector方法,vector查找方便,list插入删除方便

在这里插入图片描述

参考:
0
1
2
3
4
5

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值