侯捷C++内存分配课程总结五:std::alloc源码剖析
分析C++标准容器分配器的源码
文章内容参照于侯捷 C++内存分配系列教程
文章目录
前言
在阅读本文章之前,请确保您以及了解了alloc在内存分配时的动作。这可以参考我之前写过的文章
C++内存分配详解四:std::alloc行为剖析
文章中的源码来自于侯捷C++内存分配教程所提供的文件,注释为我本人结合侯捷的C++课程的理解,如出现错误敬请指正。
一、std::alloc的两层分配器
std::alloc在源码中共创建了两层分配器:
- 第一级的作用是处理之前说过的,申请超过最大可挂载的内存块容量(128byte)的内存
- 第二级的作用是完成std::alloc对内存的分配。
由于第二级的动作才是我们所关心的,所以我将在下边详细的分析第二级分配器的代码,而第一级分配器的代码我仅会在最后贴出
二、详细分析第二级分配器
代码取自侯捷C++内存分配教程讲义
这里为了方便阅读和分析,侯捷去除了与进程相关的代码,所以我也没有把相关代码加进去
1.类的定义、数据成员和非重点函数
/*
第二级分配器
*/
enum {
__ALIGN = 8 }; //每个链表的间隔
enum {
__MAX_BYTES = 128 }; //维护的最大的内存队列的内存
enum {
__NFREELISTS = __MAX_BYTES / __ALIGN }; //链表的个数
template<bool threads, int inst>//这里两个参数与进程相关,在当前问题中没有使用
class __default_alloc_template
{
private:
static size_t RoundUp(size_t bytes) {
//调整边界
return (((bytes)+__ALIGN - 1) &~(__ALIGN - 1));
}
private:
union obj{
union obj* free_list_link;
};//这里改用struct也行
private:
static obj* volatile free_list[__NFREELISTS];//构成内存池的16根链表的头节点
static size_t FREELIST_INDEX(size_t bytes) {
//寻找bytes所对应的链表的编号
return (((bytes)+__ALIGN - 1) / __ALIGN - 1);
}
static void *refill(size_t size);//为链表分配内存并分割挂载
static char *chunk_alloc(size_t size, int &nobjs);//为内存池分配一大块内存
static char *start_free;//pool的开头
static char *end_free;//pool的结尾
static size_t heap_size;//累计的分配量
public:
static void *allocate(size_t n);//分配内存的主函数
static void deallocate(void * p, size_t n);//回收内存的主函数
};
typedef __default_alloc_template<false, 0>alloc;//宏定义修改名称
我们这里先分析类的数据成员和非主要函数
数据成员
//这里声明了一个仅在类内部使用的共用体数据类型
//这个类型在后边将被作为内存块的指针去使用
private:
union obj{
union obj* free_list_link;
};//这里改用struct也行
源码中之所以用union而不是struct的原因是由于在之前的版本中,这个内存是需要被复用的(这里如果不清楚,可以参考我之前写过的文章:C++内存分配详解三:内存分配模型中Effective C++的实现),而到了新版本,这里的数据部分被取消了,仅用一根指针就可以了。但虽然如此,但是这个结构还是被保存下来了。
//用前边定义的共用体类型定义了内存池的总链表
private:
static obj* volatile free_list[__NFREELISTS];//构成内存池的16根链表的头节点
这里创建了一个长度为16的链表,他的每一个元素都是一根指针,以后将分别指向大小不同的内存块链表,构成总的内存池。这是当容器(或其他函数)需要分配内存时的内存来源。
这里的关键字volatile也是与进程相关的声明,此时分析时可以将他视为不存在。
//指针成员的定义
static char *start_free;//pool的开头
static char *end_free;//pool的结尾
static size_t heap_size;//累计的分配量
这里的 start_free 和 end_free 分别指向pool的开头和结尾,用这两根指针就可以维护一个pool
当pool减少时,就把start_free下移;当pool修改时,就将两根指针指向的位置修改
注意:pool不会在原来的位置上增加,需要更多的pool内存的时候,会把原来pool中的内存视为内存碎片连接到某一个链表上,然后为pool重新分配。
heap_size用于记录由alloc第二级分配器申请的内存的总大小,也就是现在alloc所可以进行管理的内存的总大小,用于后边计算追加量。
非主要函数
这里所要分析的非主要函数主要是两个:RoundUp() 和 FREELIST_INDEX()
先来看RoundUp()
//调整边界
static size_t RoundUp(size_t bytes) {
return (((bytes)+__ALIGN - 1) &~(__ALIGN - 1));
}
我给他的注释是调整边界,也就是将传入的大小提升至16的倍数
如:传入的bytes为13,那么 (13+7)&~(7) = 10100 & 11000 = 10000 即16;
这一提升保证了每一次分配出的内存都是16的倍数,也就是说每次pool中剩下的内存碎片总会有一个链表是可以挂载的。
之后是FREELIST_INDEX()
//寻找bytes所对应的链表的编号
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes)+__ALIGN - 1) / __ALI