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
2
3
4
5
6
template<class T>
class one
{
public:T x;
}
one<int> *p = new one<int>[5];

C++用了两个步骤用new实现了内存分配。
2.1 先向堆申请一块大小的内存
2.2 对其有构造函数的执行构造函数

2.1 operator new
operator new就是只申请一块大小空间,然后什么也不做,像malloc那样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
if (void* const block = malloc(size))
{
return block;
}

if (_callnewh(size) == 0)
{
if (size == SIZE_MAX)
{
__scrt_throw_std_bad_array_new_length();
}
else
{
__scrt_throw_std_bad_alloc();
}
}
}
}

2.2 placement new
placement new则是在已经申请的内存上构建对象

空间配置器

Allocator

std::Allocator是C++的默认分配器,其包含了allocate()deallocate()分别用于分配和释放内存的方法以及construct()和destroy()分别用于构造和销毁对象的方法。当我们自定义一个分配器时必须实现这四个成员函数。
其中destroy()有两个版本,这里给出第二个版本的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第二个版本的, 接受两个迭代器, 并设法找出元素的类型. 通过__type_trais<> 找出最佳措施
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last)
{
__destroy(first, last, value_type(first));
}

// 接受两个迭代器, 以__type_trais<> 判断是否有traival destructor
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor());
}

重点在于_destroy(),其中的__type_traits是用来获取迭代器所指对象的类型,根据类型的不同选择不一样的析构调用。
如当该类型的析构函数是平凡的,那么会返回__true_type,即true真值,此时__destroy_aux()将什么都不做,因为这样会高效一点。当返回false时,调用__destroy_aux()的重载

1
2
3
4
5
6
7
// 没有non-travial destructor 
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for ( ; first < last; ++first)
destroy(&*first);
}

这样做的目的是为了在范围析构的情况下节省时间和提高效率。

第一级配置器

第一级配置器在空间分配器申请大于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;

if (n > (size_t) __MAX_BYTES)
{
return(malloc_alloc::allocate(n));
}
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) // 没有多余的内存, 就先填充链表.
{
void *r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result -> free_list_link;
return (result);
};

内存池

内存池常由两个部分组成,内存块池和可用内存块列表。内存块池的内部存储结构和存储空间是连续的,但即使是相同大小之间的内存块之间的地址不一定连续。可用内存块列表是内存池块中所有空闲内存块的集合列表,用来标记可用的内存块,存储其地址,当需要申请内存块时,只需从可用内存列表中找到对应大小的空闲内存块即可。分配出去的内存块会被列表删除,释放回来的内存块会重新加入进列表。

自由链表

在学习内存池的过程中常被自由链表和内存池的关系给弄混,实际上自由链表只是内存池的一种特定实现方式。即可用内存列表用自由链表的方式存储。用一个指针数组存储每个自由链表的链表头指针,不同自由链表存储不同大小内存块的地址,每当要使用对应大小的内存块时,直接找到对应自由链表的链表头指针,将其地址返回并移除出自由链表,由其指向的下一个内存块做链表头指针。当有内存块被释放时,将其加入进对应的自由链表链表头即可,这样能够高效地添加和删除内存块,快速地管理内存空间地址。当前申请内存块链表头为空时,则会调用refill函数从堆中申请新内存块填充自由链表。

观后感

摆了3天后终于补全了hhhh。
主要学习到了内存池的结构和运行方式。原来stl容器在调用new和delete动态分配空间时是调用空间分配器重载的函数。就像vector容器在插入和删除时就要用到空间分配器了。