引言
说到C++动态内存分配,我们首先都会想到new和delete运算符,new运算符可以分配并初始化一个对象或对象数组,并返回指向数组第一个对象的指针。而delete运算符则逐个销毁(根据对象数组倒序执行析构函数)对象并回收内存,需要注意的是delete回收对象数组时应加上[]。new和delete是常用的动态内存分配手段,new将分配内存和构造对象结合,delete则将析构和回收内存结合,非常方便的同时也带来了一定的局限性。如果我们只是想分配一块内存,但是并不想马上构造对象时,new运算符的方便就显得有些多余了,这时我们需要把内存的分配与构造两个动作分离,更有效率的进行内存管理,这就需要用到allocator了。
简单的allocator实现
这里我找来了VC++2013中vector容器的原型,我们可以看到实际上vector不单单只有一个模板参数,还有一个默认的参数就是allocator类,我们完全可以DIY一个allocator来分配内存,只要这个allcator满足STL标准的接口要求。
vector<typename _Ty,typename _Alloc = allocator<_Ty>>
让我们来试试自己实现一个简单的allocator类(参考STL源码剖析)
#ifndef __JJALLOC__
#define __JJALLOC__
#include<new> // for placement new
#include<iostream> //for cerr
#include<cstddef> //for ptrdiff_t
#include<cstdlib> // for exit()
#include<climits> // for UINT_MAX
namespace JJ{
template<class T>
inline T* _allocate(ptrdiff_t size, T*){
//set_new_handler(0);
T* tmp = (T*)(::operator new)((size_t)(size * sizeof(T)));
if (tmp == 0){
std::cerr << "out of memory" << std::endl;
}
return tmp;
}
template<class T>
inline void _destory(T* ptr){
ptr->~T();
}
template<class T>
inline void _deallocate(T* buffer){
::operator delete(buffer);
}
template<class T1,class T2>
inline void _construct(T1 *p, const T2 &value){
new(p)T1(value);
}
template <class T>
class allocate{
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 diference_type;
template<class U>
struct rebind{
typedef allocator<U> other;
};
pointer allocate(size_type n, const void * hint = 0){
return _allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p, size_type n){
_deallocate(p);
}
void construct(pointer p, const_reference value){
return _construct(p, value);
}
void destroy(pointer p){
_destroy(p);
}
pointer address(reference x){
return (pointer)&x;
}
pointer const_address(const_reference x){
return (const_pointer)&x;
}
size_type max_size()const{
return (size_type)(UINT_MAX / sizeof(T));
}
};
}
这里根据STL标准所需的接口,简单的包装了new和delete运算符。主要需要讲解的就是STL标准规定的allocator类接口以及new和delete运算符的原理。
allocator类的标准接口
allocator::typedef T value_type;
allocator::typedef T* pointer;
allocator::typedef const T* const_pointer;
allocator::typedef T& reference;
allocator::typedef const T& const_reference;
allocator::typedef size_t size_type;
typedef ptrdiff_t diference_type;
allocator::rebind//嵌套的类模板,只有一个成员other,typedef allocator<U>
allocator::allocator()
//默认构造函数
allocator::allocator(const allocator&)
//拷贝构造函数
template<class U>allocator::allocator(const allocator<U>&)
//泛化的拷贝构造函数
allocator::~allocator()
//析构函数
poiniter allocator::address(reference x) const
//返回某个对象的地址。a.address(x) 等同于 &x
poiniter allocator::const_address(const_reference x) const
void allocator::allocate(size_type n,const void *hint =0)
//配置足以存储n个T对象的空间
void allocator::deallocate(pointer p,size_type n)
//回收之前配置的空间
void allocator::construct(pointer p,const T& x)
//等同于new((void*)p) T(x)
void allocator::destroy(pointer p)
//等同于p->~T()
实际上接口的作用并不难理解,倒是new的用法有些让人困惑了。new((void*)p) T(x)
到底是什么意思?::operator new又是什么呢?
new和delete的工作机理
当我们使用new表达式分配内存时,实际上执行了三个步骤。
1. 是调用名为operator new(或者 operator new[])的标准库函数。该函数分配一块原始的足够大的未构造内存
2. 编译器运行构造函数构造对象
3. 返回一个指向该对象的指针
同样的,delete则执行两个步骤:
1. 倒序执行对象的析构函数
2. 调用 operator delete或(operator delete[])标准库函数释放内存空间
我们的allocator类中就使用了operator new 和 operator delete 标准库函数,而不是直接使用new和delete表达式,目的就是将内存的分配与对象构造或者内存的回收与对象的析构分离。
这样我们就实现了非常简单的allocator类,只是将operator new和operator delete 包装了一层,没有考虑任何效率方面的提升。根据测试,我们这种粗鄙的包装效率是很低的,默认的allocator分配一千万个int只需要62ms,而我们的分配器居然需要5600ms。对于自定义的类型INT1我们的分配器需要3600ms,默认分配器只需要2600ms。
struct INT1{
int a;
INT1() :a(0){}
};
int main{
auto startTime = GetTickCount();
vector<INT1>vec(10000000,1);
cout << (GetTickCount() - startTime) << endl;
}
...
int main{
auto startTime = GetTickCount();
vector<int>vec(10000000,1);
cout << (GetTickCount() - startTime) << endl;
小结
本文简单介绍了allocator类的标准接口和简单实现,文末对比了简单实现与VC++默认的allocator效率上的差别,怎么样才能提高分配器的效率呢,之后我们会以SGI STL实现为例,改善我们的allocator类。