STL 空间配置器Allocate
template回顾
1.在类模板的实例化过程中,并不是所有成员函数在一开始就实例化了,而是在用的时候才实例化。否则容易产生很多冗余代码,这种特性叫做“延迟实例化”。
2.在使用非类型参数时,可以在编译时自动计算出参数的大小
new
1.new和malloc
在我刚开始学习面向对象的时候是只学了new,所以有时候我看到malloc会感到疑惑为什么不用new来分配内存,所以先来说一下它们的区别。
1.1 new是c++的一个关键字而malloc是一个c语言函数。
1.2 new在分配内存失败时会抛出异常,成功后还会对内存进行初始化;而malloc内存分配失败时仅返回NULL指针,且成功分配内存后不会对其初始化。这也说明了要十分注意malloc的使用,很容易因为一些细节程序崩溃。
1.3 malloc分配的内存大小是以字节数为单位,而new是以对象的大小为单位。
小结:从以上特点可以看出,在面向对象编程时new更适合用来分配内存。
2.new operator
常用的一种分配内存的方式
1 | template<class T> |
C++用了两个步骤用new实现了内存分配。
2.1 先向堆申请一块大小的内存
2.2 对其有构造函数的执行构造函数
2.1 operator new
operator new就是只申请一块大小空间,然后什么也不做,像malloc那样。
1 | void* __CRTDECL operator new(size_t const size) |
2.2 placement new
placement new则是在已经申请的内存上构建对象
空间配置器
Allocator
std::Allocator是C++的默认分配器,其包含了allocate()deallocate()分别用于分配和释放内存的方法以及construct()和destroy()分别用于构造和销毁对象的方法。当我们自定义一个分配器时必须实现这四个成员函数。
其中destroy()有两个版本,这里给出第二个版本的代码
1 | // 第二个版本的, 接受两个迭代器, 并设法找出元素的类型. 通过__type_trais<> 找出最佳措施 |
重点在于_destroy(),其中的__type_traits是用来获取迭代器所指对象的类型,根据类型的不同选择不一样的析构调用。
如当该类型的析构函数是平凡的,那么会返回__true_type,即true真值,此时__destroy_aux()将什么都不做,因为这样会高效一点。当返回false时,调用__destroy_aux()的重载
1 | // 没有non-travial destructor |
这样做的目的是为了在范围析构的情况下节省时间和提高效率。
第一级配置器
第一级配置器在空间分配器申请大于128字节空间时被调用,其中有两个函数allocate()和deallocate()分别用来申请和释放空间,在我搜索到的数据范围,大多都是认为第一级配置器是通过malloc和free来实现的,但Chat GPT的回答是new和delete,它认为malloc和free没有调用构造和析构函数容易产生内存泄漏,目前还没有分辨出哪个对错,读完源码后再回来填坑吧。
第二级配置器
第二级配置器是在申请空间大小小于128字节时调用,其调用步骤如下。
1.根据分配的内存块大小,计算所属的自由链表编号,即在free_list数组中的下标。
2.从对应的自由链表中获取一个内存块,如果自由链表中没有可用的内存块,则调用refill函数填充该链表。
3.返回获取的内存块指针。
4.如果需要分配的内存块大小超过了__MAX_BYTES(一个常数,表示内存块的最大大小,一般是128字节),则直接调用malloc_alloc::allocate函数从系统堆中分配内存块。
1 | static void * allocate(size_t n) |
内存池
内存池常由两个部分组成,内存块池和可用内存块列表。内存块池的内部存储结构和存储空间是连续的,但即使是相同大小之间的内存块之间的地址不一定连续。可用内存块列表是内存池块中所有空闲内存块的集合列表,用来标记可用的内存块,存储其地址,当需要申请内存块时,只需从可用内存列表中找到对应大小的空闲内存块即可。分配出去的内存块会被列表删除,释放回来的内存块会重新加入进列表。
自由链表
在学习内存池的过程中常被自由链表和内存池的关系给弄混,实际上自由链表只是内存池的一种特定实现方式。即可用内存列表用自由链表的方式存储。用一个指针数组存储每个自由链表的链表头指针,不同自由链表存储不同大小内存块的地址,每当要使用对应大小的内存块时,直接找到对应自由链表的链表头指针,将其地址返回并移除出自由链表,由其指向的下一个内存块做链表头指针。当有内存块被释放时,将其加入进对应的自由链表链表头即可,这样能够高效地添加和删除内存块,快速地管理内存空间地址。当前申请内存块链表头为空时,则会调用refill函数从堆中申请新内存块填充自由链表。
观后感
摆了3天后终于补全了hhhh。
主要学习到了内存池的结构和运行方式。原来stl容器在调用new和delete动态分配空间时是调用空间分配器重载的函数。就像vector容器在插入和删除时就要用到空间分配器了。