当我们 new 一个对象的时候,包含两个操作,首先 operator new 分配内存,再调用构造函数构造对象内容 但我们 delete 一个对象的时候,同样先调用析构函数,再调用 operator delete 释放内存
STL 将这个过程薄薄的包装了一下,实际上没优化
配置器定义于 中, 中又包含 <stl_alloc.h> <stl_construct.h>
内存配置由 allocator::allocate() 负责,内存释放由 allocator::deallocate() 负责 。
构造对象由 construct::construct() 负责,析构对象由 construct::destroy() 负责
对应关系:
1.allocator::allocate() 调用 ::operator new
2.allocator::deallocate() 调用 ::operator delete
3. construct::construct() 调用 new(p) T1(value);
4.construct::destroy() 调用 p->~T();
//申请内存
pointer allocate(size_type n, const void* hint = 0)
{
T* tmp = (T*)(::operator new((size_t)(n * sizeof(T))));
//operator new 和new operator是不同的
if (!tmp)
cerr << "out of memory"<<endl;
return tmp;
}
//释放内存
void deallocate(pointer p)
{
::operator delete(p);
}
//构造
void construct(pointer p, const T& value)
{
new(p) T1(value);
}
//析构
void destroy(pointer p)
{
p->~T();
}
但allocator 只是对 ::operator new、::operator delete 进行简单的包装,没有效率上的强化,还是会有内存碎片,所以设计双层配置器。
空间配置器:
第一级、第二级配置器都是以malloc、free 创建和销毁内存的。
是操作系统开辟的一大段内存空间。STL需要扩容申请内存时,就从空间配置器中申请,不需要再经过操作系统。并且,它还能回收释放的空间,供下一次使用。
优点:
1.提高效率 。 STL容器申请空间不需要频繁向操作系统申请,而是需要向空间适配器申请,只是当空间适配器空间不够时,才会向操作系统申请。空间适配器还可以回收STL容器释放的空间,供下一次使用。
2.避免内存碎片
3.更好的管理内存
空间配置器原理
空间配置器有两级结构,一级空间配置器是用来处理大块内存,二级空间配置器处理小块内存。SGI-STL规定以128字节作为小块内存和大块内存的分界线。要么选择第一级要么选择第二级配置器。
一级空间配置器
一级空间配置器原理很简单,直接是对malloc和free进行了封装,并且增加了C++中的set_new_handle思想,即申请空间失败抛异常机制。
主要的作用是:向操作系统申请内存,申请失败会抛异常。
二级空间配置器
配置内存大于128字节时,便调用第一级配置器,内存小于128字节则采用memory pool,不再求助第一配置器。SGI-STL采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度和高效管理。
内存池技术
内存池就是,先申请一块较大的内存块做为备用,当需要内存时,直接从内存池中取内存,当内存池中内存不够时,使用一级空间配置器,向内存中申请大块空间。当用户不用申请的空间时,直接归还内存池。
这样就避免了频繁向系统申请小块内存找出的效率低,内存碎片的问题。
哈希桶技术
说明:用户申请的空间基本上都会是4的整数倍,其它空间大小几乎很少用到。所以哈希桶并不需要对1~128字节的空间进行管理,也就是并不需要128个桶。SGI-STL将用户申请的内存块向上对齐到了8字节的整数倍。
为什么不用链表来管理归还的空间?
因为用户申请空间,查找合适的内存块时,效率低。
为什么向上对齐到了8字节的整数倍?
因为在哈希桶下,是以链表的形式将所有内存块连接起来的。该内存块至少需要保存下一个内存块的地址,在64位系统下,为8字节。
一个进程中有一个空间配置器,进程中所有容器需要的空间都到对应空间配置器申请。进程终止,对应空间配置器空间释放。
空间配置allocate
static void* Refill(size_t n)
{
size_t nobjs = 20;
char* chunk = (char*)ChunkAlloc(n, nobjs); //默认获得20的新节点
if (nobjs == 1) //如果只有一块直接返回调用者
return chunk;
//有多块,返回一块给调用者,其他挂在自由链表中
Obj* ret = (Obj*)chunk;
Obj* cur = (Obj*)(chunk + n);
Obj* next = cur;
Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
*myFreeList = cur;
for (size_t i = 1; i < nobjs; ++i)
{
next = (Obj*)((char*)cur + n);
cur->freeListLink = next;
cur = next;
}
cur->freeListLink = NULL;
return ret;
}
大概就是这样:
1.将n字节扩展到8的倍数,找自由链表,有则分配,没有则向内存池申请20×n的内存块
2.若内存池小于20x n,但是比一块大小n要大,那么此时将内存最大可分配的块数给自由链表
3.如果整个内存池空间都不够了,就先将内存池残余的零头给挂在自由链表上,然后向系统heap申请空间,申请成功则返回,申请失败则到自己的自由链表中看看还有没有可用区块返回,如果连自由链表都没了最后会调用一级配置器。一级配置器有out-of-memory处理机制(类似new-handler机制),或许有机会释放其他的内存拿来此处使用。如果可以就成功,否则发出bad_alloc异常。
ps:一般向系统heap申请空间,调用malloc操作重新分配内存池,大小为2倍的total_bytes为附加量,start_free指向返回的内存地址
空间释放deallocate
大于128字节需要free释放掉内存,小于128字节部分不释放内存,充当可使用的内存池。
整个流程是:
延伸:
1.为什么STL内存分配不使用new而使用malloc?
内存基本处理工具
STL有5个全局函数,
1.构造construct()
2.析构destroy()
3.uninitialized_copy() <=> copy() 会调用copy construct函数
4.uninitialized_fill() <=> fill() 会调用copy construct函数
5.uninitialized_fill_n() <=> fill_n() 会调用copy construct函数