这篇博客主要讲一下SGI-STL中的空间配置器的工作流程。我自己实现模仿STL实现了一个空间配置器,并且用两个容器list和vector测试使用了空间配置器。
这里只给出模型,如果要看源码,点击打开链接下载。源码中的注释比较多,也比较全。
下面是工程目录:
现在介绍一下工程目录:
- List.h、Vector.h是模仿STL容器中的两个list和vector的接口完成的;
- Allocate.h里面定义了内存池即空间配置器的两级实现;
- Construct.h里面主要实现了对象的构造与析构接口;
- Iterator.h里面有五种迭代器的定义、迭代器萃取、反向迭代器定义、迭代器型别萃取以及两个非常有用的函数 Distance、Advance;
- TypeTraits.h定义了迭代器所指节点的值类型萃取,是否是POD类型;
- Uninitialized.h里面是主要是一些拷贝、填充类的函数,这个在Vector中用的比较多;
- Main.cpp是测试文件,用来测试两个容器的接口和内存池实现正确与否。
下面这幅图主要是空间配置器的框架:
我们对比上面这幅图从下往上讲:
int *pi1 = SimpleAlloc<int, Alloc>::Allocate(100); // * 申请一个有100个元素的整型数组
int *pi2 = SimpleAlloc<int, Alloc>::Allocate(); // * 申请一个整型
SimpleAlloc<int, Alloc>::Deallocate(pi1, sizeof(int) * 100);
SimpleAlloc<int, Alloc>::Deallocate(pi2);
第一行语句申请了有100个整型元素的数组,那就是申请了 sizeof(int) * 100 == 400个字节的空间。由于400大于128,二级空间配置器会转去找一级空间配置器去申请,二级空间配置器之后就不管申请空间的事了。一级空间配置器直接用malloc向操作系统申请了400字节的空间(能不能申请成功就是另一回事了,在一级空间配置器的代码中其实设置了用户自定义的函数指针,如果malloc失败,就会调用这个函数指针所指的函数,该函数会做一些事,让操作系统收回一些内存,然后会再次malloc,直至成功)。
第二行语句是申请了一个整型的空间,二级空间配置器发现sizeof(int) == 4字节,是小于128字节的,那空间配置器就会在自由链表中找到4字节对应的下标(其实自由链表是没有4字节的内存块的,那就把它定位到足够满足4字节的最小的内存块,即8字节),所以下标定位到0,即在free_list[0]中找内存。 刚开始时,自由链表为空,并没有内存块,那就去找内存池要,要多少呢?要 20*8个字节,这时候发现内存池刚开始也没有内存,这时候就会去找操作系统申请内存,申请了20 * 8 == 160个字节(其实内存池是向操作系统申请了 2 * 160 == 320字节的空间,但是只返回20个对象的大小即160字节给自由链表,剩下的160字节留在了内存池中),就把最开始的8个字节返回给应用程序,剩下的152字节正好够19个整型对象的空间,就把这些空间挂接到自由链表的0号下标下。
第三行是释放申请的400字节,直接由一级空间配置器free掉,还给操作系统;
第四行是释放pi2所指向的8个字节,直接还给自由链表,并不还给操作系统。(其实有二级空间配置器申请的内存释放时都不会还给操作系统,而是还给自由链表,内存池和自由链表的内存都是到程序结束时才归还给操作系统)。
内存池优缺点:
- 优点
对于频繁地申请小块内存,减少了申请的时间。 - 缺点
由于自由链表的内存块大小不连续(8、16、24 …),导致了内部碎片的产生。这使得内存利用率不高。
小块内存释放之后,并没有归还给操作系统,而是放到了自由链表中,会导致系统内存越来越少,除非到程序结束,否则内存不会归还给操作系统。