本篇文章基于源码来剖析标准库中内存分配器的实现原理及使用。
说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。
还是来先通过思维导图来看一下本篇文章会从哪些方面来讲解stl中内存分配器和萃取器,如下:
其实stl中有关内存申请的操作是包含两个内容的:内存分配器、内存萃取器。
前面的文章中说了,vector容器本质上是个动态数组,它其实就是使用标准库的内存分配器实现的,还是先看一下代码,如下:
vector继承于_Vector_base
,而_Vector_base
中内存分配又是结构体_Vector_impl
实现的,这个结构体继承于_Tp_alloc_type
,类型_Tp_alloc_type
的完整类型是__gnu_cxx::__alloc_traits<_Alloc>::template rebind<_Tp>::other
,而根据函数_M_allocate
最终实现内存分配是通过这样一个方式实现的,如下:
不弄清楚这行代码到底是怎么回事,我们没法知道这个内存到底是怎么被分配的,而stl中其他的很多容器也都是使用这个分配器实现的,所以说,不弄清楚这个分配器是怎么回事,没法讲明白容器的使用。
想要知道他们到底是啥,首先要弄清楚他们之间的关系,我对stl源代码进行了追根溯源。
说实话,为了搞清楚这个关系,我浪费了不少脑细胞,毕竟这些个类型真的是太长了,看的眼晕,最后我画出了一张图,如下:
这么难记的类型要是用文字描述会疯掉的,还是用图片描述比较好,哈哈哈,这个类继承关系应该是一目了然了吧,包括各个类型在哪个头文件也标注的很清楚啦,自然的,对于萃取器和分配器到底是啥,我们也有了一个初步的概念了,比如我们知道了第一章中类型__alloc_traits
是萃取器,而类型_Tp_alloc_type
就是分配器啦,至于到底为啥叫分配和萃取,请继续往下看哦。
不过这里有一点,我们需要说明一下,先看头文件allocator.h
里面这段代码:
这里allocator
的基类明明是__allocator_base<_Tp>
,为啥我们图片里面不是呢,这就需要头文件new_allocator_base.h
里面的第二段代码啦,如下:
原来类型__allocator_base
是类new_allocator
的别名,所以就有了我们图片里面的这个继承关系啦。
我们接着第一章的内容,截取stl_vector.h
头文件中部分代码如下:
很显然,这里首先要清楚类型_Tp_alloc_type
到底是个怎么回事,这又是一个比较漫长的套娃过程,看下图:
所以最后这个other类型实际上就是allocator<_Tp1>
这个类型,注意它是先取了allocator<void>
这个,继而才走到带模板的allocator
那里去的,而结合vector实现代码和上面图片可知模板实参_Tp1
这个就是我们定义一个vector的时候指定的模板形参,这里以vector<int>
为例,那么这个other其实就是allocator<int>
类型了,所以_Tp_alloc_type
实际上是allocator<int>
类型,有些书上把这个套娃的过程称为萃取,所以我这里称__alloc_traits
这个为萃取器,它取到了一个分配器。
把上面调用过程转换一下,就是这样了:
看一下__gnu_cxx::__alloc_traits::allocate
的实现,如下:
所以实际上是调用了allocator<int>.allocate
这个函数来实现的内存分配,而class allocator
本身是没有这个函数的,只有它的基类new_allocator
才有这个函数,实现如下:
到最后,其实就是直接调用了::operator new
这个函数进行了内存的分配,所以allocator叫做内存分配器。
呼,总算把这个分配器和萃取器的运作过程讲完啦,举一反三,那么释放这个动态内存其实也是一样的过程哈,这里不再多说。
对于内存分配器,前面也说了,分配调用allocate函数,最终是调用了operator new
,释放内存是调用了operator delete
这个函数,所以这里不再多说。
接下来我们看一下给分配的这个动态内存中构造数据和析构数据是怎么操作的,截取代码如下:
具体怎么操作的,注释已经写得很清楚了,这里不再多说,同时get到了new的新用法呀,如下:
这样也是可以滴。
这里为什么要把max_size
这个函数拿出来说明了,因为在使用内存分配器的容器中,往往这些容器的最大元素个数都是不能超过这个函数返回值的,所以要拿出来说明一下,实现如下:
首先看size_t,之所以用size_t,是为了跨平台,每个平台定义的size_t类型可能都不一样,但一般来讲size_t是一个无符号整型的数字,假设它是一个unsigned long
,那就是4294967295 ,再除以这个元素的大小,就得出了一个容器能保存的最大的元素个数了
我们直接使用一下这个类看下,简单使用代码如下:
其实我也不知道呀,我猜是为了保持各个容器分配都有一个统一的接口,也就是标准化。
好了,有关标准库中内存分配器和萃取器的介绍就到这里了,因为没有留言功能,如果有问题需要咨询的,可以通过公众号菜单【联系作者】获取作者联系方式进行咨询哈。