前言
在c++中,可以使用new
在堆上构建一个新的对象,其实new
包含两个步骤
- 分配内存
- 使用constructor在内存中构建对象内容
而delete
也是如此
- 调用
deconstructor
将对象析构 - 释放内存
知道了new
和delete
的原理,可以手动模拟一下
1 | class Test { |
STL中的空间配置器
在STL中,为了妥善地管理内存,有专门的STL allocator,这个是为STL中的各种容器服务的
然后关于分配内存和构造STL allocator
决定将这两个阶段分开,内存配置器定义在<memory>
之中,<memory>
包含
: 负责内存分配 : 负责对象构造,析构 : 常用的工具
stl_construct.h
如上提到的,此头文件包含的函数负责对象的构造与析构
1 | //构造 |
构造
1 | inline void _Construct(_T1* __p, const _T2& __value) { |
可以看到,使用的都是placement new
,和上文我们所做的一样,那么构造函数是用在哪里的呢?答案是用在STL容器里面的,例如vector
的push_back
里就有
1 | void push_back(const _Tp& __x) { |
析构
想象一下,如何对vector
中的内容进行析构
首先,要给出范围 【1】,传入first
和last
两个Iterator,假设类型是vector<Test>::iterator=Test*
1 | template <class _ForwardIterator> |
可以看到上面的函数对迭代器进行【2】VALUE_TYPE(__value_type的宏)
取value_type
, __value_type
如【3】所示,此时,【4】_Iter = Test *
然后对其进行【5】iterator_traits
,匹配到【6】的特化版本,返回的value_type
是Test
然后对其进行【7】type_traits
看看有没有trivial
(无用的,可忽略的)析构函数
type_traits
只对POD类型进行了特化,因此Test
匹配到了泛化版本,如【8】所示
1 | template <class _ForwardIterator, class _Tp> |
可以看到,STL取最保守的值,也就是说,除非有特化,不然都会认为构造函数是no-trivial
(重要的)
例如Test *
没有经过特化,那么得到的结果就是has_trivial_destructor = __false_type
然后,就会开始选择析构的方式
1 | template <class _ForwardIterator> |
std_alloc.h
此文件包含内存的配置与释放,设计如下
- 向system heap索取空间
- 考虑多线程状态
- 考虑内存不足的应变措施
- 考虑内存碎片问题
以下讨论排除多线程状态
SGI以malloc
和free
完成内存的配置和释放,但是考虑到内存碎片问题,SGI设计了双层配置器
- 一级配置器使用
malloc
和free
- 二级配置器使用复杂的内存池
当申请内存超过128byte,使用一级配置器,小于则使用二级配置器,然而整个设计是否采用二级配置器,取决于__USE_MALLOC
具体可以看下面,展示了什么情况使用二级配置器
1 | ---------stl_alloc.h |
一级配置器
一级配置器为template <int __inst> class __malloc_alloc_template
首先定义所抛出的异常bad_alloc
1 |
然后是分配和释放的接口
1 | static void* allocate(size_t __n) |
下面是处理失败的函数,当然你也可以自己指定处理方式
1 | static void (* __set_malloc_handler(void (*__f)()))() |
默认的处理方式
1 | template <int __inst> |
也就是说,默认内存不够就会抛出异常,当然如果你设置了处理函数,就会用处理函数去处理
设计内存不足的处理方式是你的责任
二级配置器
二级配置器为
template <bool threads, int inst>class __default_alloc_template
二级配置器使用内存池,每次配置一大块内存,然后将其分成分成16个链表,各自管理的大小为8的倍数
也就是说每一个链表管理一种大小的内存块,16个链表管理{8,16,….,128}byte的小型内存块链
1 | enum {_ALIGN = 8}; |
其中内存块链的节点为_Obj_
,链表采用线性表+链表的方式管理,其中链的初始化为
1 | {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; |
1 | union _Obj { |
根据索取的内存块大小决定从哪一条链上索取内存块
例如索取128byte的块,则定位到 index = (128+7)/8 - 1 = 15的链
1 | static size_t _S_freelist_index(size_t __bytes) { |
关于内存池的管理
1 | // 发现链上没有可用块,就重新填充 |
内存配置函数
1 | static void* allocate(size_t __n) |
重新填充
如果在分配的时候发现链上没有可用块了,就调用该函数重现填充链表
默认填充的数目是20,如果内存池内存不够,那么获得的块少于20
1 | template <bool __threads, int __inst> |
内存池
_S_chunk_alloc
是从内存池中获取空间交给free list
1 | template <bool __threads, int __inst> |
stl_uninitialized
这里有三个用于为初始化的空间的函数
三者都有一个特点,就是如果过程中产生一次失败(会抛出异常),则将之前所做的全部撤销
具体是使用
1 | define __STL_UNWIND(action) catch(...) { action; throw; } |
该宏捕获所有异常,并执行action
uninitialized_copy
可以将某个范围的对象拷贝到未初始化的内存,如果拷贝存在一次失败,那么就不构造任何东西
1 | //参数模式类似于前面提到过的析构,同样是去value type |
两个特化版本,const char , const wchar_t,对于这两个类型,最有效的方法是直接移动内存内容,使用memmove
1 |
|
uninitialized_fill
该函数的作用是将某个范围的内存初始化为某个值
1 | template <class _ForwardIter, class _Tp> |
uninitialized_fill_n
将从[firs,first+n]全部构造为相同的值,和上面的一样
1 | template <class _ForwardIter, class _Size, class _Tp> |
至此
以上大致分析完了内存构造的三个重要的块,真的是累