这篇文章参考的是侯捷的《STL源码剖析》,所以主要介绍的是SGI STL实现版本,这个版本也是g++自带的版本,另外有J.Plauger实现版本对应的是cl自带的版本,他们都是基于HP实现的版本,有兴趣可以翻翻最新的源码头文件开始处有声明。
/*
*
* Copyright (c) 1994
* Hewlett-Packard Company(这里)
*
* Permission to use, copy, modify, distribute and sell this software
* and its documentation for any purpose is hereby granted without fee,
* provided that the above copyright notice appear in all copies and
* that both that copyright notice and this permission notice appear
* in supporting documentation. Hewlett-Packard Company makes no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied warranty.
*
*
* Copyright (c) 1996
* Silicon Graphics Computer Systems, Inc.(和这里)
*
* Permission to use, copy, modify, distribute and sell this software
* and its documentation for any purpose is hereby granted without fee,
* provided that the above copyright notice appear in all copies and
* that both that copyright notice and this permission notice appear
* in supporting documentation. Silicon Graphics makes no
* representations about the suitability of this software for any
* purpose. It is provided "as is" without express or implied warranty.
*/
说到STL容器的内存分配就得先从new说起。
咱们知道,new一个对象出来,编译器会做两步工作,咱称之为“new操作”:
为该类型分配内存 在新分配的内存中构造该类型的一个对象虽然new关键字很方便,但是可以预想到,在某些应用场景中,运行时开销是不太理想的,或者说,有更好的策略可以节省开销,推迟到必要的时候再付出这部分开销。
比如vector容器,对应于c#或者java中的ArrayList,这种容器的特点是一旦new出他们来,就已经分配固定大小的内存,当若干插入元素操作之后如果空间不够,则新申请一块内存,依据具体的策略确定新申请的内存的大小,一般来说2倍于原空间大小。当然了,这是基于vector的接受一个size类型的参数的构造函数来说的。咱们先可以看看vector的默认构造函数的实现的片段:
const size_type __old_size = size();
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
iterator __new_start = _M_allocate(__len);
其中_len即为新空间大小,可以看到确实是2倍于原空间,同时也可以看到,如果size大小为0个单位,则先分配1个单位的空间,由M_allocate函数进行分配,然后复制原来的元素到新分配的内存,最后归还原来的空间:
const size_type __old_size = size();
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
iterator __new_start = _M_allocate(__len);
iterator __new_finish = __new_start;
__STL_TRY {
__new_finish = uninitialized_copy(_M_start, __position, __new_start);
construct(__new_finish, __x);
++__new_finish;
__new_finish = uninitialized_copy(__position, _M_finish, __new_finish);
}
__STL_UNWIND((destroy(__new_start,__new_finish),
_M_deallocate(__new_start,__len)));
//destroy操作负责调用对象的析构函数
destroy(begin(), end());//begin()内部为_M_start,end()内部为_M_finish
//_M_deallocate负责收回空间
_M_deallocate(_M_start, _M_end_of_storage - _M_start);
_M_start = __new_start;
_M_finish = __new_finish;
_M_end_of_storage = __new_start + __len;
再瞧瞧new的时候vector分配多少单位的内存吧
vector的默认构造函数:
typedef _Vector_base<_Tp, _Alloc> _Base;
explicit vector(const allocator_type& __a = allocator_type())
: _Base(__a) {}
Vectorbase的默认构造函数:
typedef _Alloc allocator_type;
_Vector_base(const _Alloc&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
可以看到,vector在new出来的时候是没有任何空间的,其中Mstart,Mfinish等等变量名作何意义可以去找参考相关书籍,或者请教搜索引擎。
好了,下面是接受一个size类型参数的构造函数的源码:
explicit vector(size_type __n)
: _Base(__n, allocator_type())
{ _M_finish = uninitialized_fill_n(_M_start, __n, _Tp()); }
不看实现,我们先设想一下,容器一旦new出来,必定会有若干的容器内元素,如果不采用特别的应对策略,那么在一般情况下,除了分配内存,还会有一系列的构造操作。比如,如果我们一开始就定义一个vector v(n),每一个BigObject带一个需要执行很久的默认构造函数(假设),然而,我们却只用到了其中n/2个单位的空间,那么除了n/2个单位的空间闲置之外(这个是没办法的,所以不作缺点考虑吧。),这n/2个单位空间的构造操作也没有意义了。
于是,设计者利用了c++提供的一些特性将此情况比较理想地进行了解决,即将空间分配与对象构造分开进行。
接着讲述之前我们先了解一下
construct(T* p,var t):对p所指的内存中构造一个新元素,其中,用t进行复制初始化。
destroy(T* p):与construct相反,运行p所指对象的析构函数。
注意,两者都要求p必须指向了一块儿内存。
placement new(定位new): 语法形式: new(p) type(val)。与传统的new不同的是,定位new,只在已分配的内存中构造对象,并不负责分配内存。
可以猜想到construct内部实现也许就是定位new,但是有时候想象的不一定就是正确的。。。不过。。。这次其实就是正确的:
void construct(pointer p, const Tp& val) { new(p) Tp(val); }
但是,你能想到这些,不一定能想到destroy内部就是运行析构函数而已。。。好吧,其实你是对的:
void destroy(pointer p) { _p->~Tp(); }
(喂,你前面有个代码片段已经暴露了吧~)世界有时候就是充满了恶意。
知道这几个东西之后其实stl对内存分配所做的事情就没有那么神秘了。
那么,之前既然已经知道了vector是怎么节省开销的,也知道了构造的时候其实就是用了定位new,那么,是不是内存的分配用的就是operator new呢?想想可能是这样,因为这东西其实就是用来进行内存的分配,但是不会进行构造,所以可能vector中产生一个元素其实就是operator new+placement new!嗯,但是,源码给了我们一个无情的打击,前面我们已经看到,vector的实现里,内存的分配是靠Mallocate实现,那么,Mallocate_里面是怎么回子事呢?其实:
_Tp* _M_allocate(size_t __n)
{ return _M_data_allocator.allocate(__n); } ///////////////////////////////////////////////
typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; //////////////////////////////////////////////
template<class _Tp, class _Alloc>
class simple_alloc { public:
static _Tp* allocate(size_t __n)
{ return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
static _Tp* allocate(void)
{ return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
static void deallocate(_Tp* __p, size_t __n)
{ if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
static void deallocate(_Tp* __p)
{ _Alloc::deallocate(__p, sizeof (_Tp)); }
};
可以看到,它内部使用的是一个称为simple_alloc的包装类。
到这里为止,已经没法继续追下去了,因为这里会涉及到根据不同的需求,进行不同的内存分配策略。事实上,slt中的分配策略一般来说有两种。
第一级配置器:默认策略,一会儿讲述 第二级配置器:产生原因是为了避免太多小额区块造成内存碎片(其实这里还有待深入研究,因为目前为止最新版的STL源码有这么一句话——With a reasonable compiler, this(指的就是第一级配置器) should be roughly as fast as the original STL class-specific allocators, but with less fragmentation.),以内存池进行管理,不深入讨论。第一,我本身也没弄明白这个策略的内部实现,怕hold不住。第二是因为:
Defaultalloctemplate parameters are experimental and MAY DISAPPEAR in the future. Clients should just use alloc for now.
嗯,作者都这么说了。
然而,第一级配置器是什么呢?它叫_mallocalloc_template,它是怎么进行内存分配的呢?见下:
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
上段代码来自mallocalloctemplate的内部实现,可以看到,其实它就是用的C语言中用来进行堆内存分配的malloc函数(第二行的Soom_malloc用于处理内存分配失败时的情况,不做讲解),
至于为什么选它,我猜是因为效率考虑吧,没有查证过。
至此还不能认为vector就是内部是按照这样分配的,还需要接着考证,我们回到vector,看看究竟是不是用的这个策略。
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>
{
// requirements:
可以看到,如果没有为vector指定“分配策略”,那么默认的就是STLDEFAULTALLOCATOR分配策略,这里就已经差不多得到答案了,经过一个遍历所有文件查找字符串的小脚本,终于找到组织了,位于stlconfig.h
# ifndef __STL_DEFAULT_ALLOCATOR
# ifdef __STL_USE_STD_ALLOCATORS
# define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
# else
# define __STL_DEFAULT_ALLOCATOR(T) alloc
# endif
# endif
可以看到,这里的预编译命令,官方的解释是
_STLUSESGIALLOCATORS is a hook so that users can disable new-style allocators, and continue to use the same kind of allocators as before, without having to edit library headers.
并且在stlvector中,也针对性地根据是否定义STLUSESTDALLOCATORS给出了两个版本的Vectorbase,其在分配内存上的不同仅仅是一个用的是标准接口,一个是自己定的接口(但是追溯其源码,还是用的标准接口,有兴趣的同学可以瞧瞧去),但是咱们不必关心这之间的区别。
现在我们就需要弄清楚,alloc到底是啥,为什么不讨论allocator呢?因为其内部还是alloc。在stl_alloc中,我们看到
# ifdef __USE_MALLOC
typedef malloc_alloc alloc;
typedef malloc_alloc single_client_alloc;
# else
# ifdef __USE_MALLOC typedef malloc_alloc alloc;
typedef malloc_alloc single_client_alloc; # else
噢噢噢,mallocalloc,就是你了!嗯,写到这里脑子都散黄了,思绪已经拉不回来了,也不知道我究竟是在写什么了,如果您发现了我文章组织有些问题,没关系,可以提意见,反正我又不听。(/#=皿=)/|_|
PS:对了,为什么只讲了vector?因为其内部实现是固定分配的,比较有代表性,而链表实现的(如List等)虽然也是分配和构造分开的,但是逻辑上看还是一起的,所以可能没有讲的必要,知道allocator差不多就知道容器的内存分配大概状况了,所以主要内容其实并不是在容器本身了。