空间配置器,迭代器,容器,适配器

一、空间配置器

下面先总体介绍一下空间配置器。空间配置器的作用是在底层为上层的各种容器提供存储空间,需要多少分配多少,一般分配的比你需要的更多。打个不恰当的比喻,空间配置器和容器之间的关系,相当于将军和粮草的关系。当然了,容器相当于将军,它在阵前杀敌,冲锋陷阵,处理各种事情;而空间配置器就相当于粮草,给前阵提供源源不断的供给;如果一个将军想打胜仗,那必须后方粮草充足才行。

空间配置器有两种,一种是一级空间配置器,另一种是二级空间配置器。这两种空间配置器的分水岭在于用户申请的内存空间大小,如果申请的字节数大于128bytes,那么使用一级空间配置器;反之则使用二级空间配置器。下面分别来介绍一下这两种空间配置器。注:空间配置器的源码在 <Stl_alloc.h> 这个头文件中.

1.一级空间配置器

一级空间配置器__malloc_alloc_template,使用情况是在需要分配的字节数超过128bytes时,它有两个关键的函数,一个是allocate()函数,另一个是deacllocate()函数。

(1)allocate()函数

这个函数的功能是申请内存空间,由于需要的内存空间较大(大于128bytes),所以在这个函数里面调用的malloc()函数直接分配内存空间。这样会存在一个问题,就是如果内存空间不够的话怎么办?我们能想到的问题,STL的设计者早就想到了,这个时候对调用_S_oom_malloc()这个函数,这个函数中会有一个死循环,这个死循环的作用就是不断的进行malloc,只有malloc我需要的内存空间了我才会结束这个循环。

(2)deallocate()函数

这个函数的功能是释放内存空间,既然在allocate()内使用malloc()申请内存,那再这里当仁不让的应该用free()来释放内存空间,malloc()和free()就是我们生活中的好基友,成双成对出现,不然会发生内存泄露。

(3)reallocate()
我们申请的内存的时候当然也可以使用reallocate()这个函数,原理和上面类似,底层则调用的是realloc()这个函数。

2.二级空间配置器

二级空间配置器__default_alloc_template,之所以会出现二级空间配置器,原因在于要解决小块内存频繁申请和释放所带来的开销问题,以及小块内存分配造成的内存碎片问题。
对于内存频繁分配和回收带来的问题解决的方法是使用内存池(memory pool)。预先分配好一大块内存,当需要的时候直接进行分配,这样比临时分配产生的开销小很多。而对于小块内存分配造成的内存碎片问题,解决方式是将预先分配的一大块内存按一定的方式组织起来,便于应付各种大小的小块内存分配,这里是使用了一个有16个元素的free-list数组。

(1)空间配置函数allocate()

在二级空间配置器的空间配置函数中,会首先判断当前申请的字节数是否超过128bytes,如果超过了则调用一级空间配置器的空间配置函数。如果没有超过,则根据要申请的字节数按8字节对齐为它分配内存,比如说,如果需要申请23字节,则按8字节对齐的话就会分配24字节(下面图中的2号坑);如果需要56字节,则正好分配一个字节数为56的区块(下图中的6号),依此类推。如果内存不够,就会调用refill()函数,在这个函数里面,它会想尽一切办法,不择手段的获取一起能利用的内存空间。
在这里插入图片描述

(2)空间释放函数deallocate()

该函数首先判断区块的大小,大于128bytes就调用一级空间配置器,小于128bytes就找出对应的free list,将区块回收。

二、迭代器和traits编程技法

1.迭代器
iteratro模式的定义是:提供一种方法,使之能够依序寻访某个聚合物(容器)所含的各个元素,而又无需要改聚合物的内部表述方式。
迭代器是一种类似指针的对象,而指针的各种行为中最常见也最重要的便是解引用和成员访问。

2.Traits编程技法

简单来说,就是使用函数模板和类模板推导出类型参数的具体类型,然后通过具体的数据类型,来定义重载函数,实现在不同情况下调用合适的重载函数。
即通过函数模板来进行类型萃取,具体萃取方法这里就不细说,有兴趣的可以看《STL源码剖析》第三章。

三、序列式容器

    这是我们介绍的重点,这部分的源码不难看懂,所以在这里我只是进行一个简单的总结,以及对一些相关问题说出自己的看法和理解。

1.vector

vector本质上相当于一个动态数组,对于数组大家能想到的第一个特点无非就是随机访问,而对于插入删除操作对于数组来说是软肋。所以数组不适用于有频繁插入删除操作的情况。

vector维护的是一个连续线性空间,当内存不够时,只有一个办法就是重新寻找更大的空间(一般情况都是新配置的内存空间都是原内存空间的两倍),然后将原空间的元素拷贝过去,最后释放原内存空间。 这里提醒特别注意的一点,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了,这是程序员易犯的一个错误,务必小心。

2.list

list本质是一个双向链表,而对于链表大家也肯定很熟悉,最大的缺点是,当需要访问一个元素时需要遍历整个链表来进行查找;而它的优点则是对于插入和删除操作很方便,可以在O(1)时间内完成。
list的内存管理也比较简单,就是插入元素时就分配一个节点,然后将该节点插入list中;当删除一个元素就将该节点的内存释放掉。有种即插即用的感觉。

3.deque

deque本质是双端队列。它结合了vector和list这两者。它的特点是可以随机访问,以及可以在两端快速插入和删除。和vector相比,可以看成是一个vector数组。它的组织结构如下图,每个map元素都指向一段连续空间,那段连续空间也是我们真正存储数据的地方,因此map也称为中控器:
在这里插入图片描述
deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量的连续空间,串接在整个deque的头端或尾端。deque的最大任务就是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。
下面是中控器map、迭代器和缓冲区之间的关系:
在这里插入图片描述
接下来说说几种比较特殊的情况,在当前缓冲区满了以后如果继续插入元素,这时会重新分配一个缓冲区,并让map中的一个元素指向这片缓冲区,将新插入的元素存放在新开辟的缓冲区中。另外一种情况是,当删除缓冲区中的一个元素以后,缓冲区为空,这时会将这个缓冲区释放掉,以及释放指向这个缓冲区map元素。第三种情况是,如果再插入一个元素以后,发现map所指的缓冲区全部满了,这时候不仅要开辟新的缓冲区,还要扩展map的空间。

四、适配器

适配器本身没有数据结构,只是底层利用容器的操作来是实现特定的需求。这里介绍stack、queue和priority_queue这三种适配器。

1.stack

stack本质是一个操作受限在一端的线性表。特点就是每次操作只能在栈顶进行,底层使用容器deque来实现自己的操作。 栈没有迭代器,这个也好理解,因为迭代器对栈来说没有什么意义,因为栈的操作有特定限制,如果有迭代器则无疑会破坏这种吸限制。

2.queue

queue本质是一个操作受限在两端的线性表,而且只能在队首删除元素,只能在队尾插入元素。特点是FIFO,底层也是使用deque来实现自己的操作。 同理,队列也没有迭代器。

3.priority_queue

优先级队列,顾名思义,priority_queue是一个拥有权值观念的queue,它允许加入新元素、移除旧元素、审视元素值等功能。其内的元素按照元素的权值排雷,权值最高者,排在最前面。 默认情况下,priority_queue利用max_heap完成,后者是一个以vector表现的完全二叉树。max-heap可以满足priority_queue所需要的“权值高低自动递减排序”的特性。

五.总结

1.各种容器使用的情况

容器名 :本质 ( 适用情况)
(1)vector :动态数组( 随机访问、不适用于频繁插入删除的情况)
(2)list : 双向链表(循环) ( 适用于频繁插入删除,不适用于需要随机访问的情况)
(3)deque :双端队列 (适用于随机访问,以及在两端进行插入删除操作的情况)

2.适配器
适配器名 (底层实现)
stack (deque)
queue (deque)
priority_queue (max-heap+vector)
3.为什么STL中stack底层使用deque,而不使用vector和list?
栈只需进行入栈和出栈操作,而且这两个操作基本上在一个程序中都会有,基本不存在说一个程序只入栈而不出栈这种,对于这种情况下,如果使用vector作为底层来实现stack存在的一个很大问题。举一个例子来说的话,就是在程序中刚开始大量入栈,导致vector会开辟大量连续空间(当然这个开辟的过程还涉及重新分配),之后如果进行出栈到栈空,这时分配给vector的空间不会减小,而会一直保持之前占有的大块连续空间,导致很大的浪费。而对于deque来说就不存在这样的问题。

原文:https://blog.csdn.net/kang___xi/article/details/79564088

猜你喜欢

转载自blog.csdn.net/weixin_42323413/article/details/83793989
今日推荐