目录
[new 和 delete](#new 和 delete)
[空间配置函数 allocate](#空间配置函数 allocate)
C++标准模板库(STL)是一组高效的数据结构和算法的集合,广泛应用于C++程序设计中。STL由六大核心组件组成,分别是:
- 容器(Containers):各种数据结构,如vector,list,deque等等。
- 迭代器(Iterators):扮演容器域算法之间的胶合剂,是所谓的"泛型指针"
- 算法(Algorithms):各种常用算法,例如sort,search,copy,erase等等。
- 函数对象(Function Objects):又名为仿函数,行为类似函数,可作为算法的某种策略。
- 适配器(Adapters):一种用来修饰容器或者仿函数或迭代器接口的东西,例如:stack和queue。
- 分配器(Allocators):负责空间配置域管理,从实现角度来看他是一个管理空间的模板类。
STL六大组件的交互关系,容器通过分配器获取数据存储空间,算法则通过迭代器去存取容器的内容,(这也是为什么说迭代器是类似于一种胶合剂的角色),仿函数则可以协助算法完成不同的策略变化,适配器可以修饰或者套接仿函数。
分配器(Allocators)
分配器负责管理内存的分配和释放。STL 提供了 std::allocator
类模板,用于为容器分配和释放内存。分配器的设计是为了提高内存分配的效率,并且可以被重载以适应特定的应用需求。在STL的角度来看,分配器是一个非常隐秘的组件,它默默地在后台工作,精确地分配和控制内存空间。当我们深入了解STL时,分配器是我们首先应该关注的,因为它掌管着最重要的数据空间。
注:以下内容部分参考自《STL源码剖析》。
new
和 delete
一般来说,我们通常使用 new
和 delete
来分配和释放内存,例如:
cpp
class F {};
F *f = new F;
delete f;
这里的 new
操作包含了两个阶段:
- 调用
::operator new
分配内存。 - 调用
class F
的构造函数构造内容。
delete
操作则是相反的过程,即 new
变为 delete
,构造变为析构。
为了更精细地分工,allocator
将这两个阶段的操作区分开来:
- 内存分配操作由
alloc::allocate()
负责。 - 内存释放操作由
alloc::deallocate()
负责。 - 对象构造由
::construct()
负责。 - 对象析构操作由
::destroy()
负责。
构造和析构
stl_construct.h
定义了两个基本函数 construct
和 destroy
,它们分别用于对象的构造和析构。具体来说:
construct
:用于在分配的内存中构造对象。destroy
:用于在分配的内存中析构对象。
内存的配置和释放
对象构造前的空间配置和对象析构后的空间释放由 stl_alloc.h
负责。设计时考虑了以下几点:
- 向系统堆请求空间。
- 考虑多线程状态。
- 考虑内存不足时的应变措施。
- 考虑过多小型区块可能造成的内存碎片。
构造和析构过程
空间配置与释放的过程
C++的内存配置基本操作是 ::operator new(),内存释放的基本操作是 ::operator delete()。这两个全局函数相当于C语言中的malloc() 和 free()函数。
考虑到小型区块所可能造成的内存破碎的问题,C++设计了双层的配置器,第一层则是由malloc和free来完成。第二层则较为复杂以下,会根据不同的情况采取不同的策略。
当配置区块大小超过128bytes时,视为大,调用一级配置器,也就是malloc和free。
当配置区块小于128bytes时,则视为小,为了降低负担则使用memory pool整理方式,而不再求助于一级配置器。其中还配置了一级配置器和二级配置器的开放权限。关键在于__USE_MALLOC. (这名字还真是朴实无华-.-)
其中 __malloc_alloc_template就是第一级的配置器,__default开头的就是第二季的配置器。
他们的关系如下图:
第一配置器源码分析
第一配置器以malloc,free,realloc等C函数执行实际的内存配置,释放等操作。
cpp
#include <new>
#include <iostream>
// 定义内存不足时的异常处理
#ifndef THROW_BAD_ALLOC
#define THROW_BAD_ALLOC std::cerr << "Out of memory" << std::endl; exit(1)
#endif
// malloc-based allocator,通常比稍后介绍的 default allocator 速度慢
// 通常线程安全,并且对于空间的运用比较高效
template <int inst>
class malloc_alloc_template {
private:
// 以下都是函数指针,所代表的函数将用来处理内存不足的情况
static void* (*oom_malloc)(size_t);
static void* (*oom_realloc)(void*, size_t);
static void (*oom_free)(void*);
public:
// 第一级配置器直接使用 malloc()
static void* allocate(size_t n) {
void* result = ::malloc(n);
if (0 == result) {
result = oom_malloc(n);
}
return result;
}
// 第一级配置器直接使用 free()
static void deallocate(void* p, size_t /* n */) {
::free(p);
}
// 第一级配置器直接使用 realloc()
static void* reallocate(void* p, size_t /* old_sz */, size_t new_sz) {
void* result = ::realloc(p, new_sz);
if (0 == result) {
result = oom_realloc(p, new_sz);
}
return result;
}
// 以下仿真 C++ 的 set_new_handler。换句话说,你可以通过它指定你自己的 out-of-memory handler
static void (*set_malloc_handler(void (*f)())) {
void (*old)(void) = malloc_alloc_oom_handler;
malloc_alloc_oom_handler = f;
return old;
}
// malloc_alloc out-of-memory handling 初值为0,有待客户端设定
static void (*malloc_alloc_oom_handler)();
};
// 定义内存不足时的处理函数
template <int inst>
void* malloc_alloc_template<inst>::oom_malloc(size_t n) {
void (*my_malloc_handler)();
void* result = nullptr;
while (true) {
my_malloc_handler = malloc_alloc_oom_handler;
if (0 == my_malloc_handler) {
THROW_BAD_ALLOC;
}
// 调用处理例程,企图释放内存
(*my_malloc_handler)();
// 再次尝试配置内存
result = ::malloc(n);
if (result) {
return result;
}
}
}
template <int inst>
void* malloc_alloc_template<inst>::oom_realloc(void* p, size_t n) {
void (*my_malloc_handler)();
void* result = nullptr;
while (true) {
my_malloc_handler = malloc_alloc_oom_handler;
if (0 == my_malloc_handler) {
THROW_BAD_ALLOC;
}
// 调用处理例程,企图释放内存
(*my_malloc_handler)();
// 再次尝试配置内存
result = ::realloc(p, n);
if (result) {
return result;
}
}
}
// 注意,以下直接将参数 inst 指定为 0
typedef malloc_alloc_template<0> malloc_alloc;
第二配置器源码分析
第二级配置器多了一些机制,主要是为了避免太多小额区块造成的内存碎片,小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题。额外的空间无法避免,毕竟系统要靠这多出来的进行管理内存。所以当区域块越小是,这块空间就越来越显得浪费。
SGI第二级配置器的做法是,如果区块够大,超过128byies时,就移交第一级配置器处理。当区块小于128bytes时,则以内存池(memorypool)管理,此法又称为次层配置(sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-lisr)。下次若再有相同大小的内存需求,就直接从fee-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中--是的,别忘了,配置器除了负责配置,也负责回收,为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes),并维护 16个/ree-lisrs,各自管理大小分别为8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 bytes 的小额区块。
大佬的想法当然不止于此,为了节省内存空姐到极致,大佬也对指针下了手。每个指针都要指向一个链表那岂不是很浪费!所以为了节省,采用了union。
这样的好处就是最大限度的去节省内存,由于Uinon中可以允许不通类型的元素存在,所以它正好可以维护整个链表,而不会局限于类型。当内存块被分配给客户端时,只需要将 free_list_link
清零或设置为无效指针,就可以将其转换为客户数据存储区。反之亦然,当内存块被释放时,可以将其转换回链表节点。
空间配置函数 allocate
作为一个配置器,__default_alloc_template拥有配置器的标准接口函数allocate,此函数会先去判断区块大小,大于128就调第一级配置器,否则就检查对应的free-list,如果free-list有可用区块就会直接拿来用,否则九八区块大小上调的8的倍数。然后使用refill味free list填充空间。
空间释放函数 deallocate()
这个函数则是比较简单一些,老样子还是先检查区块大小,如果超过128那就直接去找一级配置器进行回收,小于128则找出对应free-list进行回收。
内存池
这也是内存分配中的一个很重要的点,它的职责就是从储存内存。而chunk_alloc是负责调用内存池的一个函数,主要是负责给free-list进行空间配置。
cpp
// 分配内存
char* chunk_alloc(size_t size, int& nobjs) {
size_t total_bytes = size * nobjs;
size_t bytes_left = end_free - start_free;
if (bytes_left >= total_bytes) {
// 内存池剩余空间完全满足需求量
char* result = start_free;
start_free += total_bytes;
return result;
} else if (bytes_left >= size) {
// 内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块
nobjs = bytes_left / size;
total_bytes = size * nobjs;
char* result = start_free;
start_free += total_bytes;
return result;
} else {
// 内存池剩余空间连一个区块的大小都无法提供
size_t bytes_to_get = 2 * total_bytes + (heap_size >> 4);
if (bytes_left > 0) {
// 内存池内还有一些零头,先配给适当的 free list
int index = free_list_index(bytes_left);
Obj* volatile* my_free_list = &free_lists[index];
Obj* obj = reinterpret_cast<Obj*>(start_free);
obj->free_list_link = *my_free_list;
*my_free_list = obj;
}
// 配置 heap 空间,用来补充内存池
start_free = reinterpret_cast<char*>(::malloc(bytes_to_get));
if (start_free == nullptr) {
// heap 空间不足,malloc() 失败
// 尝试从自由链表中获取内存
for (size_t i = size; i <= BLOCK_SIZE; i += ALIGN) {
int index = free_list_index(i);
Obj* volatile* my_free_list = &free_lists[index];
Obj* p = *my_free_list;
if (p != nullptr) {
*my_free_list = p->free_list_link;
start_free = reinterpret_cast<char*>(p);
end_free = start_free + i;
return chunk_alloc(size, nobjs);
}
}
// 调用第一级配置器,看看 out-of-memory 机制能否尽点力
start_free = reinterpret_cast<char*>(malloc_alloc::allocate(bytes_to_get));
if (start_free == nullptr) {
throw std::bad_alloc();
}
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return chunk_alloc(size, nobjs);
}
}
chunk_alloc函数通过end_free - start_free去判断内存池剩余的内存数量,如果数量充足,那么则直接20个区块返回的free-list。如果不足的话但是还足够供应一个以上的区块,就返回剩余的进行。如果连一个以上的都不够了那就就需要去malloc去申请内存了。
总体来说,如果满足20个,那就返回,如果不满足,但是够用,仍然返回,如果不够,那就返回剩下的,剩下的回去再申请。当然可能申请的会多一些。但是如果malloc也申请不来,那就真的是山穷水尽了,它就会四处寻找是否有可用的内存。如果实在没有那就会发出bad_alloc这个异常了。
内存处理工具:
STL 中定义了五个全局函数,用于操作未初始化的空间。这些函数对于容器的实现非常有帮助,因为它们允许在不初始化内存的情况下直接构造或填充对象。这些函数分别是 construct
、destroy
、uninitialized_copy
、uninitialized_fill
和 uninitialized_fill_n
。这些函数在容器的实现中起着至关重要的作用。但是后三个我也没看明白说实话,留给有缘人探索了就。
以上均为笔者在阅读《STL源码剖析》时的学习笔记,并非自己探索。但是读书嘛,确实时有些时候时难以理解,这篇笔记中有笔者的一些独自理解,希望可以帮到大家,希望大家给点个关注吧~