前言

本篇文章主要内容为讲述自己对于C++11内存池项目(一位大佬所写)的解析(注意这里是我记录一下自己对项目的解析)。初次上手项目,很多知识点都没有遇到过,有些地方会提供其他的博文帮助理解,有描述不清楚或存在错误的地方还请大家一一指出。


【源码剖析】MemoryPool —— 简单高效的内存池 allocator 实现

大佬项目源码:GitHub源码

本人详细注释的源码:[**GitHub地址**](CodingRookie98/MemoryPoolPractice (github.com))

项目介绍

内存池是什么

话不多说,这里摘录最具权威的原作者对于项目的解释:
**什么是内存池?什么是 C++ 的 allocator?

内存池简单说,是为了减少频繁使用 malloc/free new/delete 等系统调用而造成的性能损耗而设计的。当我们的程序需要频繁地申请和释放内存时,频繁地使用内存管理的系统调用可能会造成性能的瓶颈,嗯,是可能,毕竟操作系统的设计也不是盖的(麻麻说把话说太满会被打脸的(⊙v⊙))。内存池的思想是申请较大的一块内存(不够时继续申请),之后把内存管理放在应用层执行,减少系统调用的开销。**

allocator详解

那么,allocator 呢?它默默的工作在 C++ 所有容器的内存分配上。默默贴几个链接吧:

C++ allocator

C++学习笔记(十) 内存机制与Allocator

class templatestd::allocator

allocator_traits


当你对 allocator 有基本的了解之后,再看这个项目应该会有恍然大悟的感觉,因为这个内存池是以一个 allocator 的标准来实现的。一开始不明白项目里很多函数的定义是为了什么,结果初步了解了 allocator 后才知道大部分是标准接口。这样一个 memory pool allocator 可以与大多数 STL 容器兼容,也可以应用于你自定义的类。像作者给出的例子 —— test.cpp, 是用一个基于自己写的 stack 来做 memory pool allocator 和 std::allocator 性能的对比 —— 最后当然是 memory pool allocator 更优

内存池是一个一个的 block 以链表的形式连接起来,每一个 block 是一块大的内存,当内存池的内存不足的时候,就会向操作系统申请新的
block 加入链表。还有一个 freeSlots_ 的链表,链表里面的每一项都是对象被释放后归还给内存池的空间,内存池刚创建时
freeSlots_> 是空的,之后随着用户创建对象,再将对象释放掉,这时候要把内存归还给内存池,怎么归还呢?就是把指向这个对象的内存的指针加到
freeSlots_ 链表的前面(前插)。
用户在创建对象的时候,先检查 freeSlots_ 是否为空,不为空的时候直接取出一项作为分配出的空间。否则就在当前 block 内取出一个 Slot_ 大小的内存分配出去,如果 block 里面的内存已经使用完了呢?就向操作系统申请一个新的 block。

内存池工作期间的内存只会增长,不释放给操作系统。直到内存池销毁的时候,才把所有的 block delete 掉

注释源码: 点我到注释源码
下图左一为刚申请的Blcok↓
Blcok详情


MemoryPool.tcc

该文件对Memory.h文件中声明过的函数进行实现,在此一些简单的类函数就不再一一赘述,只挑选出部分函数给出详细剖析,其余可在最后的代码汇总中查看注释。函数中使用了大量的强制转换,这里贴几个有关博客:
【C++】四种强制类型转换
c++类型转换

allocateBlock 创建Block块

函数用于申请一块Block内存块,并使用头插法放入Blcok内存块链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
申请一块新的分区Block
*/
template <typename T, size_t BlockSize>
void MemoryPool<T, BlockSize>::allocateBlock() {
// Allocate space for the new block and store a pointer to the previous one
// 申请 BlockSize字节大小的一块空间并用char* 类型指针接收
data_pointer_ newBolck = reinterpret_cast<data_pointer_>(operator new(BlockSize));
// newBlock成为新的block内存首址
reinterpret_cast<slot_pointer_>(newBolck)->next = this->currentBlock_;
// currentBlock 更新为 newBlock的位置
currentBlock_ = reinterpret_cast<slot_pointer_>(newBolck);
// Pad block body to staisfy the alignment requirements for elements
// 保留第一个slot 用于Block链表的链接
data_pointer_ body = newBolck + sizeof(slot_pointer_);
// 求解空间对齐所需的字节数
size_type bodyPadding = padPointer(body, alignof(slot_type_));
//若出现残缺不可以作为一个slot的空间,跳过这块空间作为第一个可用slot的位置
currentSlot_ = reinterpret_cast<slot_pointer_>(body + bodyPadding);
/*
始址: newBlock 块大小 BlockSize 末址 newBlock + BlockSize 末址减去一个slot槽大小
得到倒数第二个slot的末址 再加一得到最后一块slot的始址
*/
lastSlot_ = reinterpret_cast<slot_pointer_>(newBolck + BlockSize - sizeof(slot_type_) + 1);
}

这里需要注意的是:在生成一块BlockSize字节大小的Block内存块时需要保留Slot类型大小的空间用于后续Block链表的链接。对应图中的蓝色内存区。此外,在调用padPointer函数 的时候传入的参数使用了 C++11 的新特性 C++11 内存对齐 alignof alignas


padPointer 空间对齐

每一个Block内存块的大小(BlockSize字节)并不是刚好为Slot槽大小的整数倍(取决于使用者定义的BlockSize),例如一个内存块Block大小设置为BlockSize = 11 bytes,每个Slot槽存放数据的大小为4字节时,BlockSize % sizeof(Slot) = 11 % 4 = 2 … 3,余下的三个字节是不足一个Slot槽所需空间的,这时我们必须略去这些多余的空间。上述空间在图中用橙色区域进行了表示。
在这里插入图片描述

1
2
3
4
5
6
7
//计算对齐所需补的空间
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::size_type
MemoryPool<T, BlockSize>::padPointer(data_pointer_ p, size_type align) const noexcept {
uintptr_t result = reinterpret_cast<uintptr_t>(p); //将指针转换为整数,计算其所占有的空间
return ((align - result) % result); //多余不够一个Slot槽大小的空间,需要跳过
}

MemoryPool 构造函数

这里说明一下内存池中使用到的指针,先看看原作者是怎么描述的:

1
2
3
4
slot_pointer_   currentBlock_; // 指向第一块 Block 内存块, 内存块链表的头指针
slot_pointer_ currentSlot_; // 指向第一块可用的Slot槽,元素链表头指针
slot_pointer_ lastSlot_; // 指向该Block中最后一块可用的Slot槽,可存放元素的最后指针
slot_pointer_ freeSlot_; // 被释放元素的存放的空闲链表, 为链表头指针

当时阅读项目时很不解的点就是 currentSlot 指针的含义,作者给出的解释是元素链表的头指针 ,我思来想去,每一个Slot槽是一个联合体:

1
2
3
4
union Slot_ {
value_type element;//使用时为 value_type 类型
Slot_* next;//需要回收时为 Slot_* 类型并加入 空闲链表中
};

Slot槽
当Slot用来存放数据时,根据联合的特性,另一个指针属性是不可以使用的,这两个属性的关系也很简单,不可同时出现。因此当Slot槽存储数据时不可能同时是一个链表(链表至少需要一个指针),正是这样,再根据后面对于currentSlot指针的使用我将其解释为
指向第一个可用元素 Slot ,这是狭义上的第一个可用,在后面allocate函数中为每一个元素分配空间的时候currentSlot指针是明确的只能往后走的,也就是说这个 “可用” 不包括已经在空闲链表上的Slot(使用过但现在释放并还给内存池的槽),指向的是从没有被置放过数据的Slot槽。

1
2
3
4
5
6
7
8
// 构造函数,初始化所有指针为 nullptr
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool() noexcept {
currentBlock_ = nullptr; //指向第一块Block区 即Block内存块链表的头指针
currentSlot_ = nullptr; //当前第一个可用槽的位置
lastSlot_ = nullptr; //最后可用Slot的位置
freeSlot_ = nullptr; //空闲链表头指针
}

allocate 内存分配

为每个element元素分配内存,先查询槽的空闲链表是否为空,若不为空则直接将链表头结点表示的空间分配出去,反之到Block中分割新的Slot大小的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::pointer
MemoryPool<T, BlockSize>::allocate(size_type n, constPointer hint) {
pointer result;
if (freeSlot_ != nullptr) {
result = reinterpret_cast<pointer>(freeSlot_);
freeSlot_ = freeSlot_->next;
} else {
// 如果当前可用slot槽已经到了最后的位置,用完之后需要再创建一块Block方便后续操作
if (currentBlock_ >= lastSlot_) {
allocateBlock();
//返回分配的空间并更新 currentSlot
//currentSlot 指向Block内存块中最前面可用的Slot槽 (freeSlot链表中不算)
result = reinterpret_cast<pointer>(currentBlock_++);
}
}
return result;
}

当 currentSlot 到了 lastSlot 的位置表明该Block即将用完,必须另起炉灶,再向操作系统申请新的Block内存块。这里有一个疑点

1
2
if (currentSlot_ >= lastSlot_)
allocateBlock();

也就是说在 currentSlot 指针等于 lastSlot 的时候调用allocateBlock函数,而在此时这块空间还没有被分配出去,调用函数allocateBlock时我们注意到在函数中currentSlot指针被修改指向了新的内存块Block中的空间了!!!
currentSlot_ = reinterpret_cast(body + bodyPadding);
也就是说之前的内存块中最后一个Slot槽大小的空间还未被分配出去就已经改变了currentSlot指针,于是我将if条件修改为了

1
2
if (currentSlot_ > lastSlot_)
allocateBlock();

再次运行时就报错了,,,在这里插入图片描述

deallocate 释放内存

这里的释放内存并不是将这块空间真正的释放掉了(归还给操作系统),而是归还给内存池。这样使得内存池的空间可复用性更高,不需要频繁机械地向OS申请内存,那么这里又是如何将释放掉的Slot槽回收起来呢?考虑到Slot槽并不一定是连续释放的,因此使用链表的形式对这些离散分布的内存槽进行连接,既然是链表,那么必须有指针来完成链表的链接工作,这个指针就大有来头了,回顾一下 Slot 槽的内部结构:

1
2
3
4
union Slot_ {
value_type element;//使用时为 value_type 类型
Slot_* next;//需要回收时为 Slot_* 类型并加入 空闲链表中
};

既然使用了联合,那么很显然就是用来节省内存的,说到这里可以猜测到槽中的 Slot* 属性是用来充当链表中的指针,当我们创建对象的时候使用 value_type 也就是普通值类型,而销毁数据之后就可以使用它的另一属性将其放入空闲链表中。下图为一块Block块空闲链表的链接情况:
空闲链表
deallocate具体操作:

1
2
3
4
5
6
7
8
9
template <typename T, size_t BlockSize>
inline void MemoryPool<T, BlockSize>::deallocate(pointer p, size_type n) {
if (p != nullptr) {
//union指针域被赋值 之前的数据销毁
reinterpret_cast<slot_pointer_>(p)->next = freeSlot_;
//空闲链表头结点更新
freeSlot_ = reinterpret_cast<slot_pointer_>(p);
}
}

使用头插法维护空闲链表,当有空间请求时优先考虑空闲链表,若链表非空,则将链表头指针所指空间分配出去。
注意:这里的链表只有一个指针域,其实就是一串指针保存了被释放Slot的地址

max_size 计算最大可用槽的个数

先放上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//计算最大可用槽Slot的个数
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::size_type
MemoryPool<T, BlockSize>::maxSize() const noexcept {
// 无符号整型 unsiged int 类型运算,先将 -1 转换为无符号整型 即无符号整型最大值
// 无符号类型数值可以表示的Block的个数
// unsigned int MAX / BlockSize 向下取整
size_type maxBlock = -1 / BlockSize;

// (BlockSize - sizeof(data_pointer)) / sizeof(slot_type_) →指每一块Block中可以存放的slot槽个数
// 最大可以有 maxBlocks 块内存块
// 因此最大的可用槽个数 为每一块Block可以使用的Slot槽个数乘以最大可用Block
return (BlockSize - sizeof(data_pointer_) / sizeof(slot_type_) * maxBlock);
}

这个函数的作用就是通过计算确定最大可用的Slot的个数,这样可以防止越界导致数据损坏(越界之后的地址由于是循环计数机制会回到起点,对已经存储数据的空间进行破坏),这里的计算方式很有意思,我们一起看看吧

1
size_type maxBlocks = -1 / BlockSize;

这里的 size_type 是被 typedef 过的 unsigned int, BlockSize为 size_t (同无符号整型), 注意这个分数的上部,是不是很有意思,用一个负数去进行运算,最后赋值给无符号的数值类型,先别凌乱,慢慢来分析:
首先计算机内部为了能够更加轻松自然地去进行负数运算,选择原码是万万不可的,打个比方:
32位系统中原码的表示为 最高位为符号位,其余31位为数值位。
若使用原码进行计算:
在这里插入图片描述
2 + (-1)竟然等于 -3!!!很明显这不可行。最终科学家们找到一种很实用的方式就是补码运算
使用补码的好处
负数的补码等于其反码加1。
在这里插入图片描述
求得-1的补码其实就是 unsigned int 类型可以表示的最大值,那么 maxBlock 表示的就是内存池最多能生成的内存块Block的数量。而返回的是一个表达式:
return (BlockSize - sizeof(data_pointer_)) / sizeof(slot_type_) * maxBlocks;
BlockSize - sizeof(data_pointer) 表示每一个Block块除去一个指针大小之后的可用空间,“ (BlockSize - sizeof(data_pointer_)) / sizeof(slot_type_) ”表示一个Block块真正可以使用来存储数据的Slot槽的个数,每一块Block可以用的槽个数再乘上内存块的最大个数即可得到一个内存池可以使用的Slot槽的个数。
后续还有一些基本的类函数就不再一一介绍了,代码中含有注释。

~MemoryPool 析构函数

这里很有操作的是将 Slot* 类型指针转换为 void* 类型再对其释放空间,原解释是 void*类型释放不需要析构函数;

如果void 指向一个class类,那么系统由于认为void 指向一个普通的内存空间,所以释放指针时系统class的析构函数不会调用
转自 释放void*指针

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
析构函数
*/
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::~MemoryPool() noexcept {
slot_pointer_ curr = currentBlock_;
while (curr != nullptr) {
slot_pointer_ prev = curr->next;
// 转化为 void 指针,是因为 void 类型不需要调用析构函数,只释放空间
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}

MemoryPool头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H

#include <climits>
#include <cstddef>



//allocate 分配一个对象所需的内存空间
//
//deallocate 释放一个对象的内存(归还给内存池,不是给操作系统)
//
//construct 在已申请的内存空间上构造对象
//
//destroy 析构对象
//
//newElement 从内存池申请一个对象所需空间,并调用对象的构造函数
//
//deleteElement 析构对象,将内存空间归还给内存池
//
//allocateBlock 从操作系统申请一整块内存放入内存池


template <typename T, size_t BlockSize = 4096>
class MemoryPool {
public:
// 成员类型
typedef T value_type; // T的Value类型
typedef T* pointer;
typedef T& reference;
typedef const T* constPointer;
typedef const T& constReference;
typedef size_t size_type;
typedef ptrdiff_t differenceType; // 指针减法结果类型, 相当于指针间距离, 普通int类型
typedef std::false_type propagate_on_container_copy_assinment;
typedef std::true_type propagate_on_container_move_assinment;
typedef std::true_type propagate_on_container_move_swap;

/*
rebind 是一个分配器 (Allocator) 的成员模板,它可以根据相同的策略得到另外一个类型的分配器。
例如,如果你有一个 T 类型的分配器 A ,你可以用 A::rebind<U>::other 得到一个 U 类型的分配器 B 。
这样做的目的是为了让容器可以根据不同的类型分配内存,而不用关心分配器的具体实现。
*/
template <typename U> struct rebind {
typedef MemoryPool<U> other;
};

/* 成员函数 */
/*
C++ 内存池设计中成员函数不能抛出异常的原因可能有以下几点:
抛出异常会增加内存池的复杂度和开销,因为需要处理异常的传播和捕获。
抛出异常会破坏内存池的封装性和安全性,因为可能导致内存泄漏或内存池状态不一致。
抛出异常会降低内存池的可移植性和兼容性,因为不同的编译器和平台对异常的支持和处理方式可能不同 。
因此,C++ 内存池设计中通常使用 noexcept 版本的 new 操作符来分配内存,如果分配失败,返回一个 NULL 指针,而不是抛出异常。
这样可以避免上述问题,同时也可以让用户自己决定如何处理内存分配失败的情况。
*/
MemoryPool() noexcept;
MemoryPool(const MemoryPool& memoryPool) noexcept; // 拷贝构造函数
MemoryPool(MemoryPool&& memoryPool) noexcept; // 移动构造函数

/*
模板类拷贝构造函数
它接受一个不同类型的 MemoryPool 对象作为参数,并用它来初始化当前对象。这个函数有以下特点:
它是一个模板函数,可以接受任何类型的 U ,只要 U 和 MemoryPool 的模板参数类型兼容。
它是一个 noexcept 函数,表示它不会抛出任何异常,这样可以提高性能和安全性。
它是一个 const 函数,表示它不会修改参数对象的状态,这样可以保证参数对象的不变性。
*/
template <class U> MemoryPool(const MemoryPool<U>& MemoryPool) noexcept;

~MemoryPool() noexcept; // 析构函数

/*
拷贝赋值函数
C++ 内存池设计中将拷贝赋值函数弃置或删除的原因可能有以下几点:
拷贝赋值函数会导致内存池的状态不一致,因为它会改变内存池中对象的数量和位置。
拷贝赋值函数会增加内存池的开销和风险,因为它需要分配和释放内存,可能抛出异常或导致内存泄漏。
拷贝赋值函数会违反内存池的设计目的,因为内存池的目的是为了提高内存分配的效率和控制,而不是为了复制或转移对象。
因此,C++ 内存池设计中通常将拷贝赋值函数弃置或删除,以避免上述问题,同时也可以遵循“禁止拷贝”的设计原则。
*/
MemoryPool& operator=(const MemoryPool& memoryPool) = delete;
/*
移动赋值函数
*/
MemoryPool& operator=(MemoryPool&& MemoryPool) noexcept;

// 元素取地址
pointer address(reference x) const noexcept; // 返回常量引用指针地址
constPointer address(constReference x) const noexcept; // 返回非常量指针地址

// Can only allocate one object at a time. n and hint are ignored
//一次只能为一个目标分配空间 常量指针(const T* -> 不可以修改该地址存放的数据)
pointer allocate(size_type n = 1, constPointer hint = nullptr);
void deallocate(pointer p, size_type n = 1); //回收内存 归还给Block内存块

size_type maxSize() const noexcept; // 计算可使用的最大slot槽数

template <class U, class... Args> void construct(U* p, Args&&... args);
template <class U> void destroy(U* p); // 调用析构函数释放 p 所指的空间

template <class... Args> pointer newElement(Args&&... args);
void deleteElement(pointer p);

private:
union Slot_ {
value_type element; // 使用时为 value_type 类型
Slot_* next; // 需要回收时为Slot_* 类型并加入空闲链表中
};

/*
使用char类型申请内存的原因可能有以下几点:
1、char类型是最基本的数据类型,占用一个字节,可以表示任意的内存地址。
2、char类型可以方便地进行指针运算,实现内存的切割和合并。
3、char类型可以避免类型转换的开销,因为任何类型的指针都可以强制转换为char*类型。
4、char类型可以避免内存对齐的问题,因为char类型不需要对齐。
*/
typedef char* data_pointer_; // 字符类型指针
typedef Slot_ slot_type_; // 槽类型
typedef Slot_* slot_pointer_; // 槽类型指针

slot_pointer_ currentBlock_; // 指向第一块 Block 内存块, 内存块链表的头指针
slot_pointer_ currentSlot_; // 指向第一块可用的Slot槽,元素链表头指针
slot_pointer_ lastSlot_; // 指向该Block中最后一块可用的Slot槽,可存放元素的最后指针
slot_pointer_ freeSlot_; // 被释放元素的存放的空闲链表, 为链表头指针

// 计算对齐所需空间
size_type padPointer(data_pointer_ p, size_type align) const noexcept;
void allocateBlock(); // 申请内存块放进内存池

// 静态断言:当申请的BlockSize小于两个slot槽的内存时报错
static_assert(BlockSize >= 2 * sizeof(size_type), "BlockSize too small.");
};

#include "./MemoryPool.tcc"

#endif // MEMORY_POOL_H


函数实现MemoryPool.tcc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#ifndef MEMORY_POOL_TCC
#define MEMORY_POOL_TCC

#include "MemoryPool.h"

//计算对齐所需补的空间
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::size_type
MemoryPool<T, BlockSize>::padPointer(data_pointer_ p, size_type align) const noexcept {
uintptr_t result = reinterpret_cast<uintptr_t>(p); //将指针转换为整数,计算其所占有的空间
return ((align - result) % result); //多余不够一个Slot槽大小的空间,需要跳过
}

// 构造函数,初始化所有指针为 nullptr
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool() noexcept {
currentBlock_ = nullptr; //指向第一块Block区 即Block内存块链表的头指针
currentSlot_ = nullptr; //当前第一个可用槽的位置
lastSlot_ = nullptr; //最后可用Slot的位置
freeSlot_ = nullptr; //空闲链表头指针
}

/*
拷贝构造函数
这个构造函数的作用是用一个已有的内存池(memoryPool)来初始化一个新的内存池,
它使用了noexcept关键字来表明这个操作不会抛出异常。
它调用了MemoryPool()的默认构造函数来设置一些默认值
*/
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool(const MemoryPool& memoryPool)
noexcept: MemoryPool() {}


/*
移动构造函数
*/
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool(MemoryPool&& memoryPool) noexcept {
this->currentBlock_ = memoryPool.currentBlock_;
memoryPool.currentBlock_ = nullptr;
this->currentSlot_ = memoryPool.currentSlot_;
// memoryPool.currentSlot_ = nullptr; // 为什么不置为空?
this->lastSlot_ = memoryPool.lastSlot_;
this->freeSlot_ = memoryPool.freeSlot_;
}


/*
构造函数
*/
template <typename T, size_t BlockSize>
template <class U>
MemoryPool<T, BlockSize>::MemoryPool(const MemoryPool<U>& memoryPool)
noexcept: MemoryPool() {}


/*
移动赋值
*/
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>&
MemoryPool<T, BlockSize>::operator=(MemoryPool&& memoryPool) noexcept {
if (this != &memoryPool) { //自赋值检测
std::swap(this->currentBlock_, memoryPool.currentBlock_);
this->currentSlot_ = memoryPool.currentSlot_;
// memoryPool.currentSlot_ = nullptr; // 为什么不置为空?
this->lastSlot_ = memoryPool.lastSlot_;
this->freeSlot_ = memoryPool.freeSlot_;
}
}

/*
析构函数
*/
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::~MemoryPool() noexcept {
slot_pointer_ curr = currentBlock_;
while (curr != nullptr) {
slot_pointer_ prev = curr->next;
// 转化为 void 指针,是因为 void 类型不需要调用析构函数,只释放空间
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}


template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::pointer
MemoryPool<T, BlockSize>::address(reference x) const noexcept {
return &x;
}


template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::constPointer
MemoryPool<T, BlockSize>::address(constReference x) const noexcept {
return &x;
}

/*
申请一块新的分区Block
*/
template <typename T, size_t BlockSize>
void MemoryPool<T, BlockSize>::allocateBlock() {
// Allocate space for the new block and store a pointer to the previous one
// 申请 BlockSize字节大小的一块空间并用char* 类型指针接收
data_pointer_ newBolck = reinterpret_cast<data_pointer_>(operator new(BlockSize));
// newBlock成为新的block内存首址
reinterpret_cast<slot_pointer_>(newBolck)->next = this->currentBlock_;
// currentBlock 更新为 newBlock的位置
currentBlock_ = reinterpret_cast<slot_pointer_>(newBolck);
// Pad block body to staisfy the alignment requirements for elements
// 保留第一个slot 用于Block链表的链接
data_pointer_ body = newBolck + sizeof(slot_pointer_);
// 求解空间对齐所需的字节数
size_type bodyPadding = padPointer(body, alignof(slot_type_));
//若出现残缺不可以作为一个slot的空间,跳过这块空间作为第一个可用slot的位置
currentSlot_ = reinterpret_cast<slot_pointer_>(body + bodyPadding);
/*
始址: newBlock 块大小 BlockSize 末址 newBlock + BlockSize 末址减去一个slot槽大小
得到倒数第二个slot的末址 再加一得到最后一块slot的始址
*/
lastSlot_ = reinterpret_cast<slot_pointer_>(newBolck + BlockSize - sizeof(slot_type_) + 1);
}


template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::pointer
MemoryPool<T, BlockSize>::allocate(size_type n, constPointer hint) {
pointer result;
if (freeSlot_ != nullptr) {
result = reinterpret_cast<pointer>(freeSlot_);
freeSlot_ = freeSlot_->next;
} else {
// 如果当前可用slot槽已经到了最后的位置,用完之后需要再创建一块Block方便后续操作
if (currentBlock_ >= lastSlot_) {
allocateBlock();
//返回分配的空间并更新 currentSlot
//currentSlot 指向Block内存块中最前面可用的Slot槽 (freeSlot链表中不算)
result = reinterpret_cast<pointer>(currentBlock_++);
}
}
return result;
}


template <typename T, size_t BlockSize>
inline void MemoryPool<T, BlockSize>::deallocate(pointer p, size_type n) {
if (p != nullptr) {
//union指针域被赋值 之前的数据销毁
reinterpret_cast<slot_pointer_>(p)->next = freeSlot_;
//空闲链表头结点更新
freeSlot_ = reinterpret_cast<slot_pointer_>(p);
}
}


//计算最大可用槽Slot的个数
template <typename T, size_t BlockSize>
inline typename MemoryPool<T, BlockSize>::size_type
MemoryPool<T, BlockSize>::maxSize() const noexcept {
// 无符号整型 unsiged int 类型运算,先将 -1 转换为无符号整型 即无符号整型最大值
// 无符号类型数值可以表示的Block的个数
// unsigned int MAX / BlockSize 向下取整
size_type maxBlock = -1 / BlockSize;

// (BlockSize - sizeof(data_pointer)) / sizeof(slot_type_) →指每一块Block中可以存放的slot槽个数
// 最大可以有 maxBlocks 块内存块
// 因此最大的可用槽个数 为每一块Block可以使用的Slot槽个数乘以最大可用Block
return (BlockSize - sizeof(data_pointer_) / sizeof(slot_type_) * maxBlock);
}


/*
construct函数的作用是在内存池中给定的地址p处,使用new运算符和完美转发(perfect forwarding)的技术,
调用U类型的构造函数,将可变参数包args转发给U的构造函数12。这样可以在内存池中创建任意类型和数量的参数的对象。

construct函数是一个模板函数,它有两个模板参数:U和Args。U是要构造的对象的类型,Args是一个可变参数包,
表示U构造函数的参数列表。construct函数的第一个参数是一个U类型的指针,表示要构造对象的地址;
第二个参数是一个可变参数包,表示要转发给U构造函数的参数。
construct函数使用了C++11的新特性:变长模板(variadic template),完美转发(perfect forwarding),
以及new运算符的定位形式(placement new)12。
*/
template <typename T, size_t BlockSize>
template <class U, class... Args>
inline void MemoryPool<T, BlockSize>::construct(U* p, Args&&... args) {
//完美转发 保存参数原有的属性 是左值还是左值 是右值还是右值
//placement new 的应用,即在已经申请的空间上申请内存分配
new (p) U(std::forward<Args>(args)...);
}


// 调用析构函数释放指针所指的内存
template <typename T, size_t BlockSize>
template <class U>
inline void MemoryPool<T, BlockSize>::destroy(U* p) {
p->~U();
}


template <typename T, size_t BlockSize>
template <class... Args>
inline typename MemoryPool<T, BlockSize>::pointer
MemoryPool<T, BlockSize>::newElement(Args&&... args) {
pointer result = allocate();
construct<value_type>(result, std::forward<Args>(args)...); // 参数包解包
//返回每一个新申请空间的地址
return result;
}


// 删除元素
template <typename T, size_t BlockSize>
inline void MemoryPool<T, BlockSize>::deleteElement(pointer p) {
if (p != nullptr) {
p->~value_type(); // 调用值类型调用值类型的析构函数 typedef T value_type;
deallocate(p);
}
}

#endif // MEMORY_POOL_TCC

写在最后

若有错误及不详尽恳请指出,谢谢!

文章参考:

C++11实现高效内存池 - nepu_bin - 博客园 (cnblogs.com)

【源码剖析】MemoryPool —— 简单高效的内存池 allocator 实现_一线涯的博客-CSDN博客