STL之内存分配器
STL提供了很多泛型容器,如vector,list和map。程序员在使用这些容器时只需关心何时往容器内塞对象,而不用关心如何管理内存,需要用多少内存,这些STL容器极大地方便了C++程序的编写。
例如可以通过以下语句创建一个vector,它实际上是一个按需增长的动态数组,其每个元素的类型为int整型:
stl::vector<int> array;
拥有这样一个动态数组后,用户只需要调用push_back方法往里面添加对象,而不需要考虑需要多少内存。vector会根据需要自动增长内存,在array退出其作用域时也会自动销毁占有的内存,这些对于用户来说是透明的, stl容器巧妙的避开了繁琐且易出错的内存管理工作。
隐藏在这些容器后的内存管理工作是通过STL提供的一个默认的allocator实现的。当然,用户也可以定制自己的allocator,只要实现allocator模板所定义的接口方法即可, 然后通过将自定义的allocator作为模板参数传递给STL容器,创建一个使用自定义allocator的STL容器对象,如:
stl::vector<int, UserDefinedAllocator> array;
大多数情况下,STL默认的allocator就已经足够了。这个allocator是一个由两级分配器构成的内存管理器。
当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配
如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成, allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户
这样做的优点
小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,整个过程类似于批发和零售,起先是由allocator向总经商批发一定量的货物,然后零售给用户,与每次都总经商要一个货物再零售给用户的过程相比,显然是快捷了。当然,这里的一个问题时,内存池会带来一些内存的浪费,比如当只需分配一个小对象时,为了这个小对象可能要申请一大块的内存池,但这个浪费还是值得的,况且这种情况在实际应用中也并不多见。
避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。以内存池组织小对象的内存,从系统的角度看,只是一大块内存池,看不到小对象内存的分配和释放。
实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:
union obj {
union obj *free_list_link;
char client_data[1];
};
这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:obj* free_list[16];

图中的链表的每个节点是分开的,这是为了易于理解,真实情况是每个节点都是紧挨着的,因为每次分配一大块内存(用malloc),然后按固定大小切分,并且用指针将它们连接起来。图中只画了7个链表,真实情况是16个链表,各自管理着大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,128字节的区块。当需要的内存大小与这16个大小不匹配那么就向上舍入至最接近的大小。
Allocator分配算法

重新填充算法

块分配算法

假设这样一个场景,free_list[2]已经指向了大小为24字节的空闲块链表,如图1所示,当用户向allocator申请21字节大小的内存块时,allocaotr会首先检查free_list[2]并将free_list[2]所指的内存块分配给用户,然后将表头指向下一个可用的空闲块,如图2所示。注意,当内存块在链表上是,前4个字节是用作指向下一个空闲块,当分配给用户时,它是一块普通的内存区。