声明:源码同《STL源码剖析》(侯捷)
STL:
C++标准的模板库,通用性高。
常见的数据结构封装。
提供常用的通用算法。
STL六大组件:
容器 算法 迭代器 仿函数(函数对象) 适配器 配置器
空间配置器的作用:
1.提高代码复用率,功能模块化。
2.减少内存碎片问题。
比如我们list是链式结构,如果其中的成员是一个个new出来的,我们知道new的底层实现是malloc,同时也必须清楚,当malloc一块40字节空间时,操作系统为了能方便找到这块空间还会加一个结构在在这块空间中,所以开辟的空间不止40字节(应该是40+36,vs2013下)。空间利用率低。
3.效率问题:频率的分配小块内存,效率比较低。
4.容易造成内存泄漏。
统一管理更加方便。
5.调用malloc/new向系统分配的每块内存都有一些额外开销。
6.空间不足时的应对措施也合理。
一级空间配置器源码底层有一个__malloc_alloc_template模板类,里面封装了一些函数以应对空间不足的情况。
7.解决部分存在线程安全的问题
I.最初空间配置器的内部其实就是封装重载了new运算符。
底层源码框架制定死了,所以只能通过它给的接口调用相关函数。
比如allocate()函数就是用于分配内存空间,就是写死的,你只能用它。
II.处理内存碎片的问题时所用的是二级空间配置器。利用内存池的概念。
当使用空间配置器时有以下过程:
1.源码中存在一个枚举变量__MAX_BYTES(128)来区分大内存块和小内存块,凡是大于__MAX_BYTES的内存块都是属于一级空间配置器的管理范畴。
2.二级空间配置器的底层是由一个free list(哈希桶)来管理的。有一个枚举变量__ALIGN的值(8)就是表示哈希桶划分的间隔。
3.二级空间配置器中会有对应算法来计算出最合适的内存块和内存块个数,然后查询哈希桶是否有对应的内存块或者说内存块是否足够,如果没有,向系统询问调整free list。
如何调用?——refill()函数。
4.在allocate()函数调用时发现free list中没有空间是,便会呼叫refill()函数重新填充free list。——去找内存池要。
新的空间来自哪?——内存池。这部分工作交给chunk_alloc()。当然也有可能内存池(memory pool)中内存也满足不了要求。
类中利用两个静态指针 start_free 和 end_free来标识内存池。
chunk_alloc()会利用end_free - start_free来计算判断内存池的内存量:
如果存在内存但是不够,就先把能给的给出去。
如果存在内存而且够了,直接给不废话。
如果内存是一点都没了,只能重新去heap中malloc。
5.山穷水尽——如果真的二级空间配置器一点内存都分配不出来了,那么只能从一级空间配置器中来寻找。
为什么二级都没有而一级会有呢?
因为一级中有空间不足时的应对措施,所以就算是失败,也是一级空间配置器来报的错。
解惑:
为什么向上对齐ROUND_UP是八个字节而不是四个字节?
因为我们知道里面的内容是一块单链表,那么肯定有一个指针,那么为了符合64位系统,肯定是8个字节。
默认情况下用一级空间配置器还是二级空间配置器?
二级空间配置器。源码中有一个模板类——simple_alloc。
其中有一个宏——__USE_MALLOC ,如果这个宏被定义了,那么就是用一级空间适配器。
如果没被定义,那么就是用二级空间适配器。
这个宏就是决定,谁的别名时alloc。
而alloc就是STL标准库中默认类参数的成员——
template <class T,class Alloc = alloc>
class list{
...
};
空间配置器底层使用malloc实现的,所以他定义出来的东西并不是一个对象(malloc不调用构造函数)。
那么STL标准库开辟空间时是如何才能构建出对象?
利用定位new表达式,既然开辟了一个空间p,直接new(p) T1(value) 即可。
所以如果我们要构造STL对象和销毁STL对象分下列部分:
1.get_node 获取空间——construct 构造对象(定位new)
2.destroy 销毁对象——put_node 归还空间,如何归还看下一问。
二级空间配置器的释放?
首先先判断是否大于128,如果大于则用一级空间配置器的释放,如果小于则挂回free list。
我们了解了construct和destroy是构造和销毁,那么拷贝构造,整体复制,按指定个数复制,都会有定义——
即uninitialized_copy(),uninitialized_fill(),uninitialized_fill_n()。
详见《STL源码剖析》,这里不细说,其底层也是运动迭代器在[first,last)上遍历。