这篇文章参考的是侯捷的《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

噢噢噢,mallocalloc,就是你了!嗯,写到这里脑子都散黄了,思绪已经拉不回来了,也不知道我究竟是在写什么了,如果您发现了我文章组织有些问题,没关系,可以提意见,反正我又不听。(/#=皿=)/|_|

PS:对了,为什么只讲了vector?因为其内部实现是固定分配的,比较有代表性,而链表实现的(如List等)虽然也是分配和构造分开的,但是逻辑上看还是一起的,所以可能没有讲的必要,知道allocator差不多就知道容器的内存分配大概状况了,所以主要内容其实并不是在容器本身了。

STL容器的内存分配的更多相关文章

  1. C++的STL中vector内存分配方法的简单探索

    STL中vector什么时候会自动分配内存,又是怎么分配的呢? 环境:Linux  CentOS 5.2 1.代码 #include <vector> #include <stdio ...

  2. STL六大组件之——分配器(内存分配,好深奥的东西)

    SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128bytes时,视之为“足够大”,便调用第一级配置器:当配置区小于1 ...

  3. 标准非STL容器 : bitset

    1. 概念 什么是"标准非STL容器"?标准非STL容器是指"可以认为它们是容器,但是他们并不满足STL容器的所有要求".前文提到的容器适配器stack.que ...

  4. STL—— 容器(vector)的内存分配,声明时的普通构造&带参构造

    vector 的几种带参构造 & 初始化与内存分配: 1. 普通的带参构造: vector 的相关对象可以在声明时通过 vector 的带参构造函数进行内存分配,如下: 1 #include ...

  5. C++ STL vector 内存分配

    vector为了支持快速的随机访问,vector容器的元素以连续方式存放,每一个元素都紧挨着前一个元素存储. 当vector添加一个元素时,为了满足连续存放这个特性,都需要重新分配空间.拷贝元素.撤销 ...

  6. STL容器存储的内容动态分配情况下的内存管理

    主要分两种情况:存储的内容是指针:存储的内容是实际对象. 看以下两段代码, typedef pair<VirObjTYPE, std::list<CheckID>*> VirO ...

  7. 解析STL中典型的内存分配

    1 vector 在C++中使用vector应该是非常频繁的,但是你是否知道vector在计算内存分配是如何么? 在c++中vector是非常类似数组,但是他比数组更加灵活,这就表现在他的大小是可以自 ...

  8. SGI STL 内存分配方式及malloc底层实现分析

    在STL中考虑到小型区块所可能造成的内存碎片问题,SGI STL设计了双层级配置器,第一级配置器直接使用malloc()和free();第二级配置器则视情况采用不同的策略:当配置区块超过128byte ...

  9. (转)C++ STL中的vector的内存分配与释放

    C++ STL中的vector的内存分配与释放http://www.cnblogs.com/biyeymyhjob/archive/2012/09/12/2674004.html 1.vector的内 ...

随机推荐

  1. iOS self和super的区别

    self和super的区别 #import <Foundation/Foundation.h> 首先先写两个类 fist和two,two继承fist类 @interface First:N ...

  2. oracle体系结构

    oracle体系结构有四个部分组成分别为:oracle 服务器.用户进程.服务器进程.其他关键文件.其中oracle服务器又有实例(instance)和database组成是一个数据库管理系统. 一. ...

  3. IScroll5+在ios、android点击(click)事件不兼容解决方法

    Bug描述: ios.android4.4+下不能触发click事件. Bug解决: 调用iscroll插件,增加配置参数:click:true/false click的值是要根据移动终端设备进行判断 ...

  4. 一元云购qq互联回调地址错误解决办法

    经过追踪,点击登录后调用 system/modules/api/下面的qqlogin.action.class.php 里面又调用了qq 互联php接口样例里的QC.php的QC类的方法qq_logi ...

  5. python运维开发(六)----模块续

    内容目录 反射 模块 os模块 sys模块 md5加密模块 re正则匹配模块 configparse模块 xml模块 shutil模块 subprocess模块 反射 利用字符串的形式去对象(模块)中 ...

  6. arm-linux-gcc 安装和测试

    下载交叉编译器http://pan.baidu.com/share/link?shareid=984027778&uk=388424485 第一步进行解压: tar -zxvf 文件 第二部将 ...

  7. MFC 双缓冲加载背景

    首先定义DCmemDc和Bitmap CDC DCmemDc: CBitmap memBitmap; CBitmap *oldBitmap; 然后创建一个适应当前内存的DCmemDc CDC * dc ...

  8. Android 打包签名 从生成keystore到完成签名

    进入生成工具:  工具帮助:   输入指令并获得结果:   转自: http://www.cppblog.com/fwxjj/archive/2010/05/24/116208.html 首先,我们需 ...

  9. LDA Gibbs Sampling

    注意:$\alpha$和$\beta$已知,常用为(和LDA EM算法不同) 1.   为什么可用 LDA模型求解的目标为得到$\phi$和$\theta$ 假设现在已知每个单词对应的主题$z$,则可 ...

  10. 普林斯顿大学算法课 Algorithm Part I Week 3 排序稳定性 Stability

    稳定性(Stability):先按性质A排序,再按性质B排序,性质B相同的那些项是否仍然是按性质A排序的? 一个稳定的排序,相同值的元素应仍保持相对顺序(relative order) 稳定的算法:插 ...