前言
该系列的博客主要是看SGISTL3.0
源码,并参考了一些相关的博客还有《STL源码剖析》一书得到的心得及一些自己的理解整理而成,如有错误请指出,谢谢。
allocator
引入
事实上,我们在学习C++的时候,接触到空间配置器应该就是allocator
,它用于申请一块未初始化的空间,这是因为new
会执行两个动作,首先申请空间,其次调用其构造函数。但是如果有些空间根本就不会用到,那么调用构造函数初始化会浪费效率。而allocator
解决了这个问题,即分配/释放内存时只调用malloc
和free
,而将构造/析构动作分开。(其实new
和free
一般也是调用这两个函数,但是又额外增加了初始化以及析构动作)
代码实例
#include <iostream>
using namespace std;
#include <memory>
#include <vector>
int main()
{
//定义一个配置器对象
allocator<vector<int> > alloc;
//使用该配置器分配3个未初始化的vector<int>
vector<int>* p = alloc.allocate(3);
vector<int>* q = p;
for(int i = 0; i < 3; i++)
{
//调用vector<int>的构造函数
alloc.construct(q++, 1, i*10);
//输出(此时相当于是二维数组p[3][])
cout << p[i][0] << endl;
}
//循环调用析构函数
while(q != p)
{
alloc.destroy(--q);
}
//释放分配的空间
alloc.deallocate(p, 3);
return 0;
}
编译链接g++ -std=c++11 allocator.cpp
以上便是allocator
空间配置器最简单的使用。从中我们可以看出要使用allocator
配置器,需要包含头文件<memory>
。以及有如下几个基本的接口:
allocate(size_t n)
:用于分配n个对象construct(T* p, args)
:用于构造类型为T*的指针p指向的对象,args
是构造函数的参数destroy(T* p)
:即对类型为T*的指针p指向的对象调用对应的析构函数deallocate(T* p, size_t n)
:用于释放内存,需要注意指针p
指向的一定要是分配时的起始地址,否则可能造成未定义的结果。
深入源码
下面我们就来看看allocator
的那些接口是如何实现的,首先我们还是先从construct()
和destroy()
入手,因为allocate()
稍微复杂一些,涉及了两级配置器,第一级用于申请超过128bytes的空间,第二级用于申请小空间,采用了内存池技术。
- construct()
template <class T1, class T2>
inline void construct(T1* p, const T2& value)
{
new (p) T1(value);
}
代码其实就一行new (p) T1(value)
,但是new
不是会分配新的内存吗,你可能会有这样的疑惑。其实这里的new
跟你认识的operator new
不太一样,而是placement new
,它重载了operator new
,用于在一个已经分配好的内存中(堆/栈)构造一个新的对象。这也是一种显示调用构造/析构函数的一种方法,另一种直接在函数前加作用域。那么这样我相信你就可以理解了。(ps:使用placement new
需要包含头文件<new.h>
)
destroy
destroy
其实有多个版本,针对了不同的情况。- 传入一个指针
- 传入两个迭代器,有trivial destructor
- 传入两个迭代器,有non-trivial destructor
- 迭代器是char *和wchar *
情况一:
template<class T> inline void destroy(T* pointer) { pointer->~T(); }
这种情况没什么好说的
情况二和情况三的共同部分:
在具体到情况二和三之前,有一些公共的内部接口template <class ForwardIterator> inline void destroy(ForwardIterator first, ForwardIterator last) { __destroy(first, last, value_type(first)); } 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
,它内部调用__destroy
,value_type()
用于获取迭代器所指对象的类型,运用traits技法实现,至于具体如何实,在分析迭代器时会讲解。而在__destroy
中,内部调用__destroy_aux
,最后一个参数是trivial_destructor()
,其实这是一个typedef
,跟__type_traits
有关,也放在后面讲解,现在你只用知道它的作用是判断是否有trivial destructor就行了。情况二:
template <class ForwardIterator> inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) { for(; first < last; ++first) { destroy(&*first); } }
这个情况是针对non-trivial destructor,意思也就是在[first,last)范围内,调用情况一的函数,即依次调用析构函数。看第三个参数
__false_type
,很明显,它是一个对象,嗯….从中你可能会看出点什么。
情况三:template <class ForwardIterator> inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type){}
这里唯一可以让我们关注的就只有
__true_type
了,跟情况二结合不难发现,通过trivial_destructor()
的返回值是__true_type
还是__false_type
,而选择不同的情况。而trivial_destructor
其实是个typedef
定义的,根据不同的情况,分别为__true_type
和__false_type
,比如基础类型int之类的,肯定就typedef成__true_type
了。__true_type
和__false_type
是两个空类。trivial_destructor()
即构造一个对象。
情况四:inline void destroy(char *, char *){} inline void destroy(wchar_t *, wchar_t *){}
如你所见,这两种类型根本不需要做什么。
分了这几种情况,主要是在效率方面上的考虑,有一些对象本来的析构函数就属于没什么用的,调用这种析构函数只会浪费时间,特别是在范围析构的时候。
小结
在本小节中,我们主要了解了allocator
的简单的用法以及construct
和destroy
函数针对不同的情况采取的最具效率的做法。前面已经提到过分为两级配置器,所以allocate
和deallocate
对应的也应该有两种,一种是第一级配置器,另一种是第二级配置器。在下节中,我们将看看第一级配置器是如何分配以及释放内存的。