写在前面
最近在阅读候捷老师的《STL源码剖析》,阅读到空间配置器(allocator)这一章,觉得非常受用。这篇文章就当作自己的学习笔记,
下面我会从头到尾梳理一遍 allocator 的重点和难点,希望对这方面不了解的读者看完后有所收获。
什么是 allocator?allocator 有什么用?
我们需要对 C++ 的 allocator 的堆内存接口调用顺序有个清晰的认识,如下图所示。
allocator 堆内存管理接口 STL 的容器(eg: vector、stack、deque等)有一个共同特征,就是它们的大小可以在程序运行时改变。通俗点说就是当我们想要往容器中加东西的时候,容器的内存就会自动扩充,不需要提前设定好内存的大小。这种内存分配方式称为动态内存分配,而 allocator 正是用于动态内存的分配与释放。
有必要自己实现 allocator 吗?
在我们使用容器的时候,一般不需要我们自己去实现 allocator,程序会调用默认的 std::allocator 来动态分配内存。但是有些情况需要我们自定义 allocator,比如:
- 有些嵌入式平台没有提供默认的 malloc/free 等底层内存管理函数,你需要继承 std::allocator,并封装自定义版本的 malloc/free 等更底层的堆内存管理函数。
2. 使用 C++ 实现自己的数据结构,有时我们需要扩展(继承) std::allocator。
3. 大部分用 C++ 写的游戏程序都有自己重新实现的 allocator。
自定义 allocator 的难点
代码就是《STL源码剖析》上的代码,自己添加了一些注释。另外代码中有几个难点值得我们探讨。
难点一:
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
std::set_new_handler(0); // 分配失败,抛出std::bad_alloc
// 空间的分配实现,调用 ::operator new() 全局函数
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
std::cerr << "out of memory" << std::endl;
exit(1);
}
return tmp;
}
我们来看看这段代码的第 3 行:
std::set_new_handler(0);
在解释 set_new_handler 函数之前,我们先来看一段标准库函数声明。
namespace std{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
从声明中可以看出,new_handler 是个 typedef,定义出一个指针指向函数,该函数没有参数也没有返回值。
而 set_new_handler 是“获得一个 new_handler 并返回一个 new_handler”的函数。
这样就很清晰了,在 operator new 分配内存失败抛出一个异常之前,会先调用一个错误处理函数(即 new_handler),但是 new_handler 无参数无返回,所以需要调用 set_new_handler 函数来获取和返回 new_handler。
难点二:
template <class T1, class T2>
inline void _construct(T1* p, const T2& value)
{
new(p) T1(value); // placement new
}
代码里的第 4 行比较少见,它是一个重载函数,叫作 placement new。
再解释之前,我们先来区分一下 new、operator new、placement 究竟有什么区别?
new operator 是一个我们熟悉的 new,不可以重载,作用是调用 operator new 申请内存,并初始化一般用户调用。
operator new 是重载函数,一般在类中进行重载。如果类中没有重载 operator new,那么调用的就是全局的 ::operator new 来完成堆的分配。
placement new 是 operator new 的一个重载版本,只是我们很少用到它。如果你想在已经分配的内存中创建一个对象,使用 new 是不行的。也就是说 placement new 允许你在一个已经分配好的内存中构造一个新的对象。
我们知道使用 new 操作符分配内存需要在堆中查找足够大的剩余空间,这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。
placement new 就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。
所以,placement new 非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。
难点三:
template <class U>
struct rebind {
typedef allocator<U> other;
};
rebind 的意义就在于实现两个不同但两者互相有关的类型(比如类型 T 和 Node类型),使用同一种内存分配方法。如果抛开 rebind 不提,想要实现上述的意义,容器必须要让 allocator 是同一个模板,问题就出在容器并不关心你的 allocator 是怎么写的。
它唯一有关的就是在声明时在 template 中写 alloc=allocator<T>,只知道模板参数名 allocator,而不知道其具体实现,导致没有办法让 T 与 U 的 allocator 是同一个。于是在 allocator<T> 中创建一个 U 的 allocator,标准中有这样的规定:
对于 allocator<T> 与一个类型 U,allocator<U> 与 allocator<T>::rebind<U>::other 是等价的。在想使用 allocator<U> 的时候就需要使用 allocator<T>::rebind<U>::other,否则就是用了一个别的 allocator了。
源代码
myalloc.h 头文件
#ifndef MY_ALLOCATOR_H
#define MY_ALLOCATOR_H
#include <new> // placement new
#include <cstddef> // ptrdiff_t, size_t
#include <cstdlib> // exit()
#include <climits> // UINT_MAX
#include <iostream> // cerr
// 一个简单的空间配置器
namespace myAllocator
{
// 空间的分配,可以存储 size 个 T 对象
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
std::set_new_handler(0); // 分配失败,抛出std::bad_alloc
// 空间的分配实现,调用 ::operator new() 全局函数
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
std::cerr << "out of memory" << std::endl;
exit(1);
}
return tmp;
}
// 空间的释放,调用 ::operator delete() 全局函数
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); // placement new
}
// 对象的析构
template <class T>
inline void _destroy(T* ptr) {
ptr->~T();
}
// 提供外部使用 allocator
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; // ptrdiff_t 是两个指针相减结果的有符号整数类型
// 嵌套 allocator
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 T& value) {
_construct(p, value);
}
void destroy(pointer p) {
_destroy(p);
}
// 返回某个对象的地址
pointer address(reference x) {
return (pointer)&x;
}
// 返回某个 const 对象的地址
const_pointer const_address(const_reference x) {
return (const_pointer)&x;
}
// 返回可成功分配的最大量
size_type max_size() const {
return size_type(UINT_MAX/sizeof(T)); // UINT_MAX 是 unsigned long 及 unsigned long long 的最大值
}
};
}
#endif
main 函数
#include "myalloc.h"
#include <vector>
#include <iostream>
using namespace std;
int main(int argc, char* argv[]){
cout << "allocator test" << endl;
vector<int,myAllocator::allocator<int> >v;
v.push_back(1);
v.push_back(2);
for (int i = 0; i < v.size(); ++i)
{
cout << v[i] << ' ';
}
cout<<endl;
return 0;
}