空间配置器
参考
STL源码剖析
空间配置器
从STL的角度看,空间配置器是一个很常见的东西,他以缺省的形式隐藏在一切组件中,默默无闻,为各个容器高效的管理空间(空间的申请与回收)
- vector
- list
- unordered_map
等等,我们平常用的时候,通常都是向内存申请空间,但是既然STL中叫做空间配置器,那么一定不单单只能申请内存空间,因为空间不一定都是内存,也可以是磁盘或者其他辅助存储的介质。
一个简单的空间配置器
所需要包含的头文件,new
,exit
,size_t
,ptrdiff_t
(两个指针之间的距离),UINT_MAX
,cerr
(标准错误)。才知道居然都在这里。。。。。。
ptrdiff_t类型变量通常用来保存两个指针减法操作的结果
对于一个简单的空间适配器来说,需要有申请空间,释放空间的操作(简单实现,只使用operator new/operator delete
,placement new
)
new/delete 与 operator new/operator delete ,placement new
- new operator和 delete operator
new operator
和delete operator
就是 new 和 delete
操作符,当我们在程序中使用new或者delete时,会先调用我们运算符重载的operator new/operator delete 函数
来申请空间,之后就调用对象的构造和析构函数去初始化和析构空间中的数据。
他和sizeof
一样是语言内置的,他总是做两件事
- 为对象申请内存
- 调用构造函数初始化对象
我们是不能对这个功能进行改变的,也就是不能重载。但是我们可以重载operator new/operator delete
- operator new/operator delete
operator new/operator delete
的本质是一个函数,他所实现的功能仅仅就是申请空间和释放空间,并不会调用相关的构造函数进行初始化工作。
当无法满足所要求分配的空间时,则
-
如果有new_handler,则调用new_handler
-
如果没要求不抛出异常(以nothrow参数表达),则执行bad_alloc异常,否则返回0
operator new
就像operator ++
一样,是可以重载的,但是如果类中没有重载operator new
,那么调用的就是全局的::operator new
来完成堆的分配
- placement new
当我们使用operator new
来申请到堆中的空间时,这个时候所申请的空间中的数据类型就是我们申请时的指定数据类型。
但是如果我们想要把这个已经申请的空间,变成存储另外一种数据类型的时候,就需要使用placement new
,来对这个空间的数据类型进行重新分配,他允许我们在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。
void *operator new( size_t, void * p ) throw() { return p; }
原型中void* p
实际上就是指向一个已经分配好的内存缓冲区的的首地址.
placement new
只是operator new
重载的一个版本,它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。对这片空间我们还是需要进行手动析构的,否则就会造成内存泄漏
源程序
#include <new> // placement new
#include <cstddef> // ptrdiff_t size_t
#include <cstdlib> // exit
#include <climits> // UINT_MAX
#include <iostream> // cerr
namespace dcl
{
// 分配内存
template<class T>
inline T* _allocate(ptrdiff_t size, T*) {
std::set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if(tmp == 0) {
std::cout<< "out of memory " << std::endl;
exit(1);
}
return tmp;
}
// 释放内存
template<class T>
inline void _deallocate(T* buffer) {
::operator delete(buffer);
}
// 以申请的内存中重新构造 T1对象
template<class T1,class T2>
inline void _construct(T1* p, const T2& value) {
new(p) T1(value);
}
// placement new 对象的析构
template<class T>
inline void _destroy(T* ptr) {
ptr->~T();
}
template <class T>
class allocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
template <class U>
struct rebind {
typedef allocator<U> other;
};
// 申请 n 个 pointer类型的空间
pointer allocate(size_type n,const void* hint = 0) {
return _allocate((difference_type)n, (pointer)0);
}
// new 空间的析构
void deallocate(pointer ptr, size_type n) {
_deallocate(ptr);
}
// 已经new 的空间重新分配
void construct(pointer ptr, const T& value) {
_construct(ptr, value);
}
// 销毁重新分配的空间
void destory(pointer p) {
_destroy(p);
}
// 得到指针变量
pointer address(reference x) {
return (pointer)&x;
}
// const 指针变量
const_pointer const_address(reference x) {
return (const_pointer)&x;
}
// 得到最大可以开辟的空间数
size_type max_size() const {
return size_type(UINT_MAX / sizeof(T));
}
};
}
测试结果:
但是在STL源码剖析
中,有这个空间配置器只能有限的搭配PJ STL
和RW STL
,而完全无法应用于SGI STL
首先最大的一个不同之处就在于写法的不同,我们的空间配置器中,对于vector容器
的第二个参数是这样描述的
vector<int, std::allocator<int> >
但是,SGI STL
中必须这样来
vector<int, std::alloc>
std::alloc
class Foo { ... };
Foo* pf = new Foo; // 配置内存,然后析构对象
delete pf; // 析构对象,然后释放内存
STL中为了分工明确,将这两个阶段的操作区分开,所以将内存的配置交给了alloc::allocate()
,内存的释放交给了alloc::deallocate()
,对象的构造交给了::construct()
,对象的析构操作由::destory()
负责
而这两种操作所对应的头文件也不同:
#include <stl_alloc.h> // 空间的配置和释放
#include <stl_construct> // 对象内容的构造和析构
空间的构造和析构
空间的构造与析构的基本工具便是construct()
和destory()
对于构造函数,STL中直接使用placement new
,分工明确,只是对已经分配好的空间,进行重新构造
但是对于析构函数,实际上除了一个指针变量的参数外,还有其他的函数重载,对于一些特殊的情况,进行了特化
进行特化的原因
如果我们申请了一块特别大的空间,但是空间中对象的析构函数都是trivial destructor
,也就是无关痛痒的析构函数,那么不停的调用每个对象的析构函数就会浪费很大的性能,所以说就需要进行特化判断
trivial destructor 与 non-trivial destructor
如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor
。
反之,如果特定定义了析构函数,则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor
。
如果某个类中只有基本类型的话是没有必要调用析构函数的,delelte p
的时候基本不会产生析构代码
而如何判断一个对象,是否定义了析构函数,则使用了一种萃取
的机制,主要就是利用了函数模板的思想,在定义类型的同时得到想要的类型。
在这一步中,首先利用value_type()
获得迭代器所指对象的类型,再利用__type_traits<T>
来判断析构函数是否无关痛痒,如果是,那就什么也不干;不是,则需要循环调用来完成对象的析构工作。
然后在分别调用对应的操作。
空间的配置与释放
SGI对于对象的析构,还有这么一套设计哲学。。。
- 向system heap 申请空间。(堆中申请空间)
- 考虑多线程状态
- 考虑内存不足时的应变措施
- 考虑
小型区块
造成的内存碎片问题
而在C++中,我们一般对内存的操作便是:::operator new()
和 ::operator delete()
。相当于在C语言中的malloc()
和free()
。
对此,由于考虑到了内存破碎
的问题,SGI设置了双层级配置器。
但是我们在使用的过程中,好像vector容器的第二个默认的缺省值只是alloc
,并没有具体说明是哪一级的配置器,使用的是哪一级的配置器我们是感受不到的,但是实际是这样的
这分别为第一级适配器与第二级适配器,所以说,alloc
是不接受任何参数的,这也是为什么我们之前的简单的配置器完全不支持SGI的原因