空间配置器
所有的STL的操作对象(所有的数值)都存放在容器内,而容器一定要配置空间才能存放资料
空间配置器的标准接口
typedef unsigned int size_t;
allocator::value_type // 数值类型 typedef T
allocator::pointer // 指针 typedef T*
allocator::const_pointer // 常指针 typedef const T*
allocator::reference // 引用 typedef T&
allocator::const_reference // 常引用 typedef const T&
allocator::size_type // 长度类型 typedef size_t
allocator::difference_type // typedef ptrdiff_t
/*ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。*/
allocator::rebind // 重新绑定
/*
rebind
一个嵌套的 nested(嵌套) class template(类模板) 。class rebind<U> 拥有唯一成员 other,那么是一个typedef(定义类型),代表allocator<U> 配置U类型的空间
给定了类型T的分配器Allocator=allocator<T>,现在想根据相同的策略得到另外一个类型U的分配器allocator<U>,那么allocator<U>=allocator<T>::Rebind<U>::other.
*/
allocator::allocator() // default constructor(默认构造函数)
allocator::allocator(const allocator&) // copy constructor(拷贝构造函数)
template<class U> allocator::allocator(const allocator<U>&) // 泛化的拷贝构造函数
allocator::~allocator() // destructor 析构函数
pointer allocator::address(reference x) const//返回对象的地址
const_pointer allocator::address(const_reference x) const // 返回const 对象的地址
pointer allocator::allocate(size_type n,const void * = 0)
/*
配置空间,足以存储n个T对象
参数提示,可以用他增进区域性
*/
void allocator::deallocator(pointer p,size_type n)// 归还先前配置的空间
size_type allocator::max_size() const //返回可成功配置的最大空间
void allocator::construct(ponter p,const T& x) // 等价于new((void *)p) T(x)
void allocator::destroy(pointer p)// 等同于p->~T()
new 空间分配
new关键字和operator new, placement new之间的种种关联
new operator:指我们在C++里通常用到的关键字,比如A* a = new A;
operator new:它是一个操作符,并且可被重载(类似加减乘除的操作符重载)
new 操作中包含 operator new
调用operator new (sizeof(A)) // operator new 是分配空间的第一个操作
调用A:A() // 调用构造函数
返回指针 // 返回指针
placement new函数此函数接收一个已构造的对象,通过拷贝构造的方式在给定的内存地址p上构造一个新对象,代码中后半截T1(value)便是placement new语法中调用构造函数的写法,如果传入的对象value正是所要求的类型T1,那么这里就相当于调用拷贝构造函数。类似的,因使用了placement new,编译器不会自动产生调用析构函数的代码
placement new本身只是返回指针p,new§ A()调用placement new之后,还会在p上调用A:A(),这里的p可以是动态分配的内存,也可以是栈中缓冲,如char buf[100]; new(buf) A();
placement new 定位放置new
一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。
定位放置new操作的语法形式不同于普通的new操作。例如,一般都用如下语句A* p=new A;申请空间,而定位放置new操作则使用如下语句A* p=new (ptr)A;申请空间,其中ptr就是程序员指定的内存首地址
placement new版本,它本质上是对operator new的重载,定义于#include 中。它不分配内存,调用合适的构造函数在ptr所指的地方构造一个对象,之后返回实参指针ptr。
语法:new(地址) 对象; 对象可以用构造函数构造
注意
(1)用定位放置new操作,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。如本例就是在栈上生成一个对象。
(2)使用语句A* p=new (mem)
A;定位生成对象时,指针p和数组名mem指向同一片存储区。所以,与其说定位放置new操作是申请空间,还不如说是利用已经请好的空间,真正的申请空间的工作是在此之前完成的。
(3)使用语句A *p=new (mem) A;
定位生成对象时,会自动调用类A的构造函数,但是由于对象的空间不会自动释放(对象实际上是借用别人的空间),所以必须显示的调用类的析构函数,如本例中的p->~A()。
(4)如果有这样一个场景,我们需要大量的申请一块类似的内存空间,然后又释放掉,比如在在一个server中对于客户端的请求,每个客户端的每一次上行数据我们都需要为此申请一块内存,当我们处理完请求给客户端下行回复时释放掉该内存,表面上看者符合c++的内存管理要求,没有什么错误,但是仔细想想很不合理,为什么我们每个请求都要重新申请一块内存呢,要知道每一次内从的申请,系统都要在内存中找到一块合适大小的连续的内存空间,这个过程是很慢的(相对而言),极端情况下,如果当前系统中有大量的内存碎片,并且我们申请的空间很大,甚至有可能失败。为什么我们不能共用一块我们事先准备好的内存呢?可以的,我们可以使用placement new来构造对象,那么就会在我们指定的内存空间中构造对象。
使用:
- 调试时使用 , 增减参数打印
class A
{
public:
A()
{
std::cout<<"call A constructor"<<std::endl;
}
~A()
{
std::cout<<"call A destructor"<<std::endl;
}
void* operator new(size_t size, const char* file, int line)
{
std::cout<<"call A::operator new on file:"<<file<<" line:"<<line<<std::endl;
return malloc(size);
return NULL;
}
};
- 内存池使用
operator new的另一个大用处就是内存池优化,内存池的一个常见策略就是分配一次性分配一块大的内存作为内存池(buffer或pool),然后重复利用该内存块,每次分配都从内存池中取出,释放则将内存块放回内存池。在我们客户端调用的是new关键字,我们可以改写operator new函数,让它从内存池中取出(当内存池不够时,再从系统堆中一次性分配一块大的),至于构造和析构则在取出的内存上进行,然后再重载operator delete,它将内存块放回内存池。 - STL 使用
在SGI STL源码中,defalloc.h和stl_construct.h中提供了最简单的空间配置器(allocator)封装
SGI STL 的空间配置器
SGI STL(gcc 中应用) 的配置器与其他的STL空间配置器不同
名称是alloc 不是allocator 不接受参数
在程序中使用注意区别
vector<int ,std::allocator<int>> iv ;// 在VC or CB 中使用空间适配器注明类型
vector<int ,std::alloc> iv; // int gcc
SGI STL alloc 不合规,但是我们使用时经常缺省使用
SGI STL 在设计时已经为每个容器指定好空间配置器了
template <class T,class Alloc = alloc>
class vector {...}
标准的allocator 定义
注意SGI STL 也有自己的空间配置器但是不使用因为效率不好
只是allocator(标准的空间配置器) 这是基层内存配置/释放行为(也就是::operator new 和 :: operator delete) 的一层封装,没有效率上的加强
-
基础空间操作
new 等价 (1)调用::operator new 配置内存 (2)调用构造函数
delete 等价 (1)调用析构函数 (2)::operator delete 释放内存 -
STL 定义时区分
alloc:: allocate() // 内存配置操作
alloc:: deallocate() // 内存释放操作
alloc:: construct() // 对象构造操作
alloc:: destroy() // 对象析构操作
配置器定义在memory中 有文件
#include<stl_alloc.h> // 负责内存空间配置与释放
#include<stl_construct.h> // 负责对象的创建于构造
// 补充
#include<stl_uninitualized.h> // 定义一些全局函数 uninitualized 用来填充拷贝大的数据块
拷贝中最差使用construct()构造 最佳使用C标准memmove() 移动
构造析构基本工具
使用 placement new 包含 new.h 文件
- construct() 工具 创建 new (p ) T1(value);
- destroy() 工具 两个版本 直接析构 或者判断类型空间长度析构
C++ 并不支持指针所有物类型判断
空间的配置与释放 std::alloc SGI 设计
设计基础
- 向system heap (堆区) 要求空间
- 考虑多线程(multii-threads) 状态
- 考虑内存不足时的应变措施
- 考虑过多"小型区块" 可能造成内存碎片问题(fragment)问题
C++ 内存配置的基础操做
::operator new()
::operator delete()
/*
相当于malloc() free()
SGI 使用malloc 和 free 完成内存申请与释放操作
*/
双层级配置器
解决小型区块所造成的的内存破碎问题
第一级配置器 直接使用malloc() 和 free() 进行空间配置
第二级配置器 根据情况选择 以128 bytes 作为区别
- 大于128 bytes 是视为足够大,调用第一级配置器
- 小于128 bytes 视为过小 为了降低额外负担overhead(用于管理内存的空间),使用复杂的 memory pool 内存池整理
设计中由__USE_MALLOC 决定只开放一级配置器还是使用同时开放第二级配置器
SGI STL 第一级配置器 malloc_alloc_template
template<int inst>
class __malloc_alloc_template{...};
/*
1. allocate() 直接使用 malloc
2. deallocate() 直接使用free
3. 和C++ set_new_handler() 处理内存不足
*/
SGI STL第二级配置器 defualt_alloc_template
template <bool threads,int inst>
class __default_alloc_template{...};
/*
4. 维护16个自由链表(free lists),负责16中小型区块的次配置能力
5. 内存池(memory pool) 以malloc() 配置而得,如果内存不足,转调用第一级配置器,有异常处理程序
6. 如果需求大于128bytes ,也调用第一级配置器
*/
内存不足处理 类似C++ new -handle 机制
类似但是不是使用,因为配置内存使用的是malloc realloc free 并不是::operator new
来配置内存的
C++ new-handle 机制——解决内存不足的特定模式
在系统调用内存配置需求无法被满足时,要求系统启用一个函数,一旦无法完成任务在抛出bad::alloc 异常状态之前,会先调用客端指点的处理程序
SGI 用malloc 来配置内存,而不是使用::operator new
来配置内存原因:
- 历史原因
- C++ 不提供realloc的内存配置操作 SGI 不能直接使用set_new_handler() 需要仿真一个
在内存不足时第一级配置器allocate() realloc()
都是调用malloc() realloc()
不成功调用oom_malloc() oom_realloc()
oom_ 程序中都有内循环,不断调用内存不足处理程序,如果内存不足处理程序没有被客端设定就调用————THROW_BAD_ALLOC 丢出bad_alloc 异常信息,或者利用exit(1) 终止程序
设计内存不足处理例程是客端的责任,设定内存不足处理程序也是客端的责任客端即操作操作系统
避免太多小区块-内存碎片问题
小区块内存碎片带来的问题:
- 内存碎片化
- 配置时造成额外负担过大,额外负担(如:cookie ,记录内存大小)
注:额外负担永远无法避免,系统要靠多出来的空间来管理内存,但是内存区块越小,额外负担就越大,显得越浪费
二级空间配置器的做法:区块大于128byte 时,交给一级空间配置器,小于128 bytes 时时使用内存池memory pool 管理,又称层次配置
层次配置——使用内存池的手段_SGI 管理维持内存块的手段
每次配置一大块内存,并维护对应的自由链表free-list 下次有相同的内存需求,就直接从自由链表中拨出,客端归还就由自由链表收回
SGI 二级空间配置,会自动把小碎块的大小上升到8的整数倍,并且维护16个自由链表,大小从 8byte 16byte 24byte 到128byte
维护free 自由链表开销的解决:
自由链表结点形式
union obj {
union obj * free_list_link;
char client_data[1];
};
使用联合体完成空间复用,第一个可以看做指向相同区块的指针,第二个可以看做指向数据的指针
obj * free_list[16] 16根指向内存的指针数组
- 空间配置函数
static void * allocate(size_t n)
{
obj * volatile * my_free_list;
obj * result;
// 大于128 就调用第一级配置器
if(n > (size_t) __MAX_BYTES)
{
return (malloc_alloc::allocate(n));
}
// 寻找16个链表中合适的
my_free_list = free_lsit + FREELIST_INDEX(n);
result = * my_free_list;
if(result == 0)
{
// 没有找到可用的free list 就重新填充 free list
void * r = refill(ROUND_UP(n));
return r;
}
// 调整 free list
*my_free_list = result -> free_list_link;
return (result);
}
- 空间释放函数
static void deallocate(void * p,size_t n)
{
obj * q = (obj *)p;
obj * volatile * my_free_list;
// 大于128byte 调用一级空间配置器
if(n > (size_t) __MAX_BYTES)
{
malloc_alloc::deallocate(p,n);
return;
}
// 寻找 free list
my_free_list = free_list + FREELIST_INDEX(n);
// 调整free list 收回区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
- free lists 的重新填充
重新填充空间取自内存池,free list 中没有空间后就调用 refill() =准备填充空间,取自内存池chunk_alloc() 取20个新的区块,内存池空间不足可能少于20块 chunk(块)
内存池-固定大小区块规划 memory pool
内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
(Memory Pool)是一种内存分配方式,又被称为固定大小区块规划(fixed-size-blocks allocation)。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
pass by reference
块空间申请函数_alloc 内存二级配置中块申请函数
template<bool threads,int inst>
char * __default_alloc_template<threads,inst>::chunk_alloc(size_t size,int &nobjs)
{
/*
size 设请的块大小 byte
nobjs 申请的块个数 使用引用传递,用于实时改变个数
*/
char * result;
size_t total_bytes = size * nobjs; // 需求空间大小
size_t bytes_left = end_free - start_free; // 内存池剩余空间
if(bytes_left >= total_bytes){
// 内存池空间满足条件
}else if(bytes_left >= size){
// 内存池剩余空间不能完全满足需求但是需要提供一个以上的区块
]else{
// 内存池剩余空间连一个都无法提供
// 剩余空间有利用价值
// 利用空间补充内存池 堆空间
}
}
内存基本管理工具
STL 定义有五个全局函数,作用于未初始化的空间
construct() // 构造
destroy() // 析构函数
uninitialized_copy() // 对应高层copy()
uninitialized_fill() // 对应高层fill()
uninitialized_fill_n() // 对应高层fill_n()
uninitialized_copy()
函数原型
template <class InputIterator,class ForwardIterator>
ForwardIterator uninitialized_copy(InputIterator first,InputIterator last,ForwardIterator result);
使我们能够将内存的配置与对象的构造行为进行分离开来
注:有commit or rollobac 要么构造出所有必要元素,要么不够造任何东西
在初始化的空间完成拷贝的功能
容器全区间构造函数range constructor
- 配置内存区块,足以包含所有元素
- 使用
uninitialized_fill()
函数原型uninitialized_copy() 在内存区块上构造元素
template <class ForwardIterator,class T>
void uninitialized_fill(ForwardIterator first,ForwardIterator last const T&x);
使我们能够将内存的配置与对象的构造行为进行分离开来
在未初始化的空间,完成填充功能
uninitialized_fill_n()
函数原型
template <class ForwardIterator,class T>
void uninitialized_fill(ForwardIterator first,Size n,const T&x);
使我们能够将内存的配置与对象的构造行为进行分离开来
完成范围填充
POD 标量型别(传统的C struct 型别)
POD指的是这样一些数据类型:基本数据类型、指针、union、数组、构造函数是 trivial 的 struct 或者 class。
POD用来表明C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。非POD数据类型与C不兼容,只能按照C++特有的方式进行使用。
POD本质就是与c兼容的数据类型
- 所有标量类型(基本类型和指针类型)、POD结构类型、POD联合类型、以及这几种类型的数组、const/volatile修饰的版 本都是POD类型。
- POD结构/联合类型:一个聚合体(包括class),它的非static成员都不是pointer to class member、pointer to class member function、非POD结构、非POD联合,以及这些类型的数组、引用、const/ volatile修饰的版本;并且,此聚合体不能有用户自定义的构造函数、析构函数、拷贝构造函数.
- POD类型可以具有static成员、成员typedef、嵌套struct/class定义和成员函数/方法。(C++标准)给出的定义:将对象的各字节拷贝到一个字节数组中,然后再将它重新拷贝到原先的对象所占的存储区中,此时该对象应该具有它原来的值。
POD类型的特点:所有POD类型都可以作为union的成员,反之,所有非POD类型都不能作为union的成员。
POD 型别必然有trivial ctor() dtor() copy() assignment() 函数
构造函数(ctor)
复制构造函数(copy)
赋值函数(assignment)
析构函数(dtor)
如果这个类都是trivial(无意义的) ctor/dtor/copy/assignment函数,我们对这个类进行构造、析构、拷贝和赋值时可以采用最有效率的方法,不调用无所事事正真的那些ctor/dtor等,而直接采用内存操作如malloc()、memcpy()等提高性能,这也是SGI STL内部干的事情。
有意义的:non-trivial 满足:显式(explict)定义了这四种函数。类里有非静态非POD的数据成员、有基类中的一条
// 非POD重载指针数值
template <</span>class T> void copy(T* source, T* destination, int n, __false_type)
{
// 省略异常处理
for (; n > 0; n--,source++,destination++)
{
// 调用source的复制构造函数
constructor(source, *destination);
}
}
// POD重载指针数值
template <</span>class T> void copy(T* source, T* destination, int n, __false_type)
{
// 省略异常处理
memmove(source, destination, n);
}