【理论学习/C++】《STL源码剖析》学习笔记:Ch2空间配置器

Ch2 空间适配器

2.1 空间配置器标准接口

必要接口
template<class T>
class allcator{
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;		//特指带模板的allcator
    };
    
    //调用申请空间函数,为指针申请空间
    pointer allocate	(size_type n,const void* hint=0);
    //调用释放空间函数,释放指针所指的空间
    void			deallocate		(pointer p,size_type n);
    //调用placement new函数,赋值操作
    void			construct		(pointer p,const T& value);
    //调用析构函数
    void			destroy			(pointer p);
    //取x的地址(取指针)
    pointer			address			(reference x);
    //取x的常地址
    const_pointer	const_address	(const_reference x);
    //返回可申请的最大空间
    size_type		maxsize()		const;
}
使用情况:

只能有限度搭配PJ STL和RW STL,不能适配SGI STL。

2.2 SGI空间适配器

SGI STL的配置器与众不同,名称是alloc。在SGI STL的容器中,缺省使用alloc。

2.2.1 std::allocator 标准空间配置器

不建议使用,效率不佳,仅仅是对new和delete运算符做了简单的包装。形式上类似于2.1的空间适配器

2.2.2 std::alloc 特殊空间配置器

一般的内存配置操作:

​ 用new申请空间,调用构造函数;调用析构函数,delete释放空间。

SGI的改进:

​ 内存配置由alloc::allocate()负责,内存释放由alloc::deallocator()负责;

​ 构造函数由construct()实现,析构函数由destroy()实现。

memory文件内容:

​ 包含三个头文件:<stl_construct.h>,<stl_alloc.h>,<stl_uninitialized.h>

​ <stl_construct.h>定义construct()和destroy(),负责对象的构造和析构

​ <stl_alloc.h>,定义一、二级配置器,名为alloc

​ <stl_uninitialized.h>定义部分全局函数,用于填充或复制内存数据

2.2.3 construct()和destroy()分析

部分原码:

#include<new.h>

template<class T1,class T2>
inline void construct(T1 *p,const T2& value){
    new (p) T1 (value);//placement new,并调用T1构造
}

//第一版本
template<class T>
inline void destroy(T* pointer){
    pointer->~T();//调用T析构函数
}
//第二版本,接受两个迭代器,设法找出元素的数值型别以求取最佳措施
template<class ForWardIterator>
inline void destroy(ForWardIterator first,ForWardIterator last){
    __destroy(first,last,value_type(first));
    //value_type为判断参数,通过重载的方式来实现分支
}

template<class ForwardIterator,class T>
inline void __destroy(ForWardIterator firstForwardIterator last,T*){
   	typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
    //trivial_destructor代表默认构造函数,这种构造函数并不重要
    __destory_aux(first,last,trivial_destructor());
}
//有non_trivial destructor,用循环逐个调用
template<class ForwardIterator>
inline void
__destory_aux(ForwardIterator first,ForwardIterator last,__false__type){
    for(;first<last;++first){
        destroy(&*first)//调用第一版本的destroy
    }
}
//有non_trivial destructor,不用逐个调用析构函数
template<class ForwardIterator>
inline void __destroy_aux(ForwardIterator,ForwardIterator,__true_type){}
//以下是destroy第二版本针对char*和wchar_t* 的特化版
inline void destroy(char*,char*){}
inline void destroy(wchar_t*,wchar_t*){}

2.2.4 std::alloc 剖析

​ SGI是以malloc()和free()的方式进行内存的配置和释放。考虑到小型区块可能造成的内存破碎的问题,SGI设计了双层配置器。

​ 第一级配置器直接使用malloc()和free(),第二级配置器采用内存池的整理方式,维护16个自由链表,每个自由链表都指向一块小的空间,用于分配空间,并进行回收处理。

​ 当配置区块超过128bytes时,调用第一级,否则调用第二级。

2.2.5第一级配置器剖析

代码:
template<int inst>
class __malloc_alloc_template{
private:
    //oom:out of memory
    static void* oom_malloc(size_t);		//处理malloc申请失败的函数
    static void* oom_realloc(void*,size_t);	//处理realloc申请失败的函数
    static void (* __malloc_alloc_oom_handler)();
    //函数指针,指向返回类型为void的无参函数,是处理oom函数
public:
    static void* allocate(size_t n){
        void* result=malloc(n);
        if(!result)
            result=oom_malloc(n);
        return result
    }//包装malloc函数,配置内存溢出处理
    
    static void deallocate(void* p,size_t n){
        free(p);
    }//包装free函数,第二个参数实际上未用到
    
    static void* reallocate(void* p,size_t old_sz,size_t new_sz){
        void* result=realloc(p,new_sz);
        if(!result)
            result=oom_realloc(p,new_sz);
        return result
    }//包装realloc函数,配置内存溢出处理
    
    static void (* set_malloc_handler(void (*f)()))(){
        void (*old)()=__malloc_alloc_oom_handler;
        _malloc_alloc_oom_handler=f;
        return(old);
    }//这是一个函数,返回类型为void(*f)(),参数为f
};

template<int inst>
void(* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)=NULL;
//初始处理函数为NULL
template<int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n){
    void (*my_malloc_handler)();
    void *result;
    
    while(true){
        my_malloc_handler=__malloc_alloc_oom_handler;
        if(!my_malloc_handler){
            THROW_BAD_ALLOC;//cerr<<"out of memory!"<<endl;exit(1)
        }//若处理函数为空,扔出BAD_ALLOC
        (*my_malloc_handler)();//否则进入处理函数,企图释放内存
        result=realloc(p,n);
        if(result)
            return result;//申请成功,则返回
    }
}
//指定inst为0
typedef __malloc_alloc_template<0> malloc_alloc
说明

​ 1.由于并没有使用new运算符,因此不能直接用c++的new-handler机制。实际上这个机制为分配失败时,在扔出std::bad_alloc之前,先调用一个指定的函数处理例程。

​ 2.之所以没有用new,一个是出于历史原因,另一个原因是new没有realloc函数的实现

2.2.6 第二级配置器剖析

代码
enum{__ALIGN=8};						//小型区块的上调边界
enum{__MAX_BYTES=128};					//小型区块的大小上界
enum{_NFREELISTS=__MAX_BYTES/__ALIGN};	//自由链表数
//第一个参数用于多线程,第二个参数没有用。
template<bool threads,int inst>
class __default_alloc_template{
private:
    staitc size_t ROUND_UP(size_t bytes){
        return((bytes+_ALIGN-1) & ~(__ALIGN-1));
    }//bytes向上调整为8的倍数
    
    union obj{
        union obj* free_list_link;	//自由链表指针
      	char client_data[1];		//展示数据
    };
    
    static obj* volatile free_list[_NFREELISTS];//自由链表的索引表
    static size_t FREELIST_INDEX(size_t bytes){
        return ((bytes+__ALIGN-1)/__ALIGN-1);
    }//大小除以上调边界,再向上取整。由于索引从0开始,故减1
    static void* refill(size_t n);//返回一个大小为n的指针
    //配置nobjs个大小为size的空间,不保证nobjs的值不变化
    static char* chunk_alloc(size_t size,int& nobjs);
    
    static char* start_free;//内存池起始位置
    static char* end_free;//内存池终止位置
    static size_t heap_size;//堆的大小
    
public:
    static void* allocate(size_t n);							//空间配置函数
    static void* deallocate(void *p,size_t n);					//空间释放函数
    static void* reallocate(void* p,size_t old_sz,size_t new_sz);
}
//static成员变量初值设定
template<bool threads,int inst>
char* default_alloc_template<threads,inst>::start_free=NULL;//起始地址
    
template<bool threads,int inst>
char* default_alloc_template<threads,inst>::end_Free=NULL;//终止地址

template<bool threads,int inst>
size_t default_alloc_template<threads,inst>::heap_size=0;//堆的大小

template<bool threads,int inst>
default_alloc_template<threads,inst>::obj* volatile
default_alloc_template<threads,inst>::free_list[__NFREELISTS]=
{NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL};
//自由链表索引表
说明:

​ 1.当区块大于128bytes时,交付给第一级配置器,否则由第二级配置器完成。

​ 2.16个自由链表分别管理8-128kb的空间(空间大小为等差数列,公差为8)。因此若申请空间的大小不是8的整数倍,会上调至8的倍数。

​ 3.自由链表采用共用体,这样obj既可以是实际的空间(char *),也可以是指向下一个obj结点(obj *)。这样做的好处是节省空间开销(因为指针占用4个字节或者8个字节,在节省空间的前提下,不可忽略)。

2.2.7空间配置函数

代码:
template<bool threads,int inst>
static void* __default_alloc_template<threads,inst>::allocate(size_t n){
    obj* volatile * my_free_list;//指向需要的结点
    obj* result;//指向需要的空间
    if(n> (size_t)__MAX_BYTES){
        return(malloc_alloc::allocate(N));
    }//大于128,调用第一级配置器
    my_free_list=free_list+FREELIST_INDEX(n);//寻找索引
    result =*my_free_list;//result指向该空间
    if(!result){
        void* r=refill(ROUND_UP(n));
        return r;
    }//自由链表无空间供使用,则从内存池中申请并配置自由链表
    *my_free_list=result->free_list_link;//取出空间后,改变该自由链表头结点的指向
    return result;
}
原理说明:

​ 1.自由链表管理着以8位单位的闲置空间,申请时,会根据向上调整的结果从适当的自由链表中寻找,若有空间则分配至并调整自由链表,否则从内存池中申请空间给申请者和自由链表。

​ 2.由于自由链表管理空间,因此申请空间对于自由链表而言需要进行删除头结点指向的结点。

步骤总结:

​ 1.调整申请空间大小为8的倍数(向上调整)

​ 2.根据调整后的大小选取适当的自由链表,判断自由链表是否能提供空间

​ 3.若能提供,则取之给申请者,同时调整自由链表指向

​ 4.若自由链表无空间给申请者,则调用refill()函数向内存池申请空间,并将申请到的空间中的1个申请者,剩下的归为自己。根据内存池和系统堆空间的情况,未必能申请到预期数量的空间。

2.2.8空间释放函数

代码:
template<bool threads,int inst>
static void __default_alloc_template<threads,inst>::deallocate(void* p,size_t n){
    obj *q=p;//辅助指针
    obj * volatile* my_free_list;//适当的自由链表
    if(n>__MAX_BYTES){
        malloc_alloc::deallocate(p,n);
        return;
    }//n大于128,调用第一级释放器
	my_free_list=free_list+FREELIST_INDEX(n);//寻找适合的自由链表
    //插入操作
    q->free_list_link=*my_free_list;
    *my_free_list=q;
}
步骤总结:

​ 1.根据调整好的空间选取合适的自由链表

​ 2.归还空间,相当于自由链表的头插法

2.2.9 refill()函数剖析

代码:
//需提前保证n是8的倍数
template<bool threads,int inst>
void* __default_alloc_template<threads,inst>::refill(size_t n){
    int nobjs=20;
    //调用chunk_alloc函数,从内存池中申请nobjs个大小为n的空间,实际上未必能申请到的空间数可能少于20
    char* chunk=chunk_alloc(n,nobjs);
    obj* volatile * my_free_list;
    obj* result;
    obj* current_obj,* next_obj;
    int i;
    //若只申请到1个,则返回之
    if(nobjs==1)
        return chunk;
    //否则,需要把多的空间交给自由链表。寻找合适的自由链表
    my_free_list =free_list+FREELIST_INDEX(n);
    //取出第一个给申请者,因此自由链表要向后移动
    result=(obj*)chunk;
    *my_free_list=next_obj=(obj*)(chunk+n);
    //依次将剩余空间插入自由链表
    for(i=1;;i++){
        //移动current_obj和next_obj
        current_obj=next_obj;
        next_obj=(obj*)((char*)next_obj+n);
        if(nobjs-1==i){
            current_obj->free_list_link=NULL;
            break;
        }//已插入nobjs-1个空间,则插入完毕,尾指针指向NULL
        else{
            current_obj->free_list_link=next_obj;
        }//否则,继续指向下一块空间
    }
    return result;
}
步骤总结:

​ 1.向内存池中申请nobjs(默认20)个大小为n(提前保证为8的倍数)的空间。(实际上申请个数可能小于20)

​ 2.对申请到的空间,分配1个给申请者,若有剩余,交付给自由链表。以尾插入的方式依次将剩余空间插入。

2.2.10内存池

chunk_alloc函数代码:
//需提前保证size是8的倍数
template<bool threads,int inst>
char* __default_alloc_template<threads,inst>::chunk_alloc(size_t size,int& nobjs){
    char* result;
    size_t total_bytes=size*nobjs;
    size_t bytes_left=end_free-start_free;
    //剩余空间大于等于总申请空间,则给予之,同时移动起始地址指针
    if(bytes_left>=total_bytes){
        result=start_free;
        start_free+=totoal_bytes;
        return result;
    }
    //否则,若尚且能够提供至少一个空间,则调整nobjs,并分配之
    else if(bytes>=size){
        nobjs=bytes_left/size;
        total_bytes=size*nobjs;
        result=start_free;
        start_free+=totoal_bytes;
        return result;
    }
    //否则,需要向系统堆空间申请空间
    else{
        //申请空间大小为需求量的两倍加上附加量
        size_t bytes_to_get=2*total_bytes+ROUND_UP(heap_size>>4);
        //如果尚且有空间,则将其移动到适当的自由链表中
        if(bytes_left>0){
            obj* volatile * my_free_list=free_list+FREELIST_INDEX(bytes_left);
            ((obj*)start_free)->free_list_link=*my_Free_list;
            *my_free_list=(obj*)start_free
        }
        //为start_free申请bytes_to_get大小的空间
        start_free=(char*)malloc(bytes_to_get);
        //若申请失败,则尝试自由链表中获取空间
        if(!start_free){
            int i;
            obj* volatile * my_free_list,*p;
            //遍历所有的自由链表
            for(i=size;i<=__MAX_BYTES;i+=__ALIGN){
                my_free_list =free_list+FREELIST_INDEX(i);
                p=*my_free_list;
                //若自由链表下有空间,将其补充至start_free中,同时调整自由链表
                if(p){
                   *my_free_list=p->free_list_link;
                    start_free=(char*)p;
                    end_free=start_free+i;
                    //递归调用本函数,这是为了修正nobjs
                    return(chunk_alloc(size,nobjs));
                }
            }
        }
        //如果仍不行,只能用第一级适配器申请一个空间,而这显然会调用handler函数,或者最终申请到空间,或者抛		  //出BAD_ALLOC
        end_free=NULL;
        start_free=(char*)malloc_alloc::allocate(bytes_to_get);
        //若最终申请到空间
        heap_size+=bytes_to_get;//改变堆空间大小
        end_free=start_free+bytes_to_get;
        return chunk_alloc(size,nobjs);//递归调用
    }
}

2.3内存基本处理工具

STL定义了五个全局函数,作用于未初始化的空间上。除了construct ()和destory ()(分别是构造函数和析构函数),还有uninitialized_copy (),uninitialized_fill (),uninitialized_fill_n ()(分别对应于高层次函数copy ()、fill ()、fill_n ())

2.3.1 uninitialized_copy()分析

参数:

​ 1.迭代器first:指向输入端起始位置

​ 2.迭代器last:指向输入端结束位置(前闭后开区间)

​ 3.迭代器result:指向输出端(欲初始化空间)的起始处

实现流程:

​ 1.先取出迭代器result的基类型,然后判断是否为POD型别

​ 2.若是,则采取最有效率的复制手法,调用高层次函数copy ()

​ 3.否则,采取最安全的方法,对于区间的每一个元素,一一调用construct函数进行构造(传值为遍历使用的迭代器cur的值,即*cur)

​ 注:对于char* 和wchar_t*两种型别,可以采取memmove ()函数来执行复制行为。SGI为这两种行为设计一份特化版本

2.3.2 uninitialized_fill ()分析

参数:

​ 1.迭代器first:指向输入端起始位置

​ 2.迭代器last:指向输入端结束位置(前闭后开区间)

​ 3.x:表示初始值

实现流程:

​ 1.同样需要判断POD型别

​ 2.若是,则采取最有效的初值填写方法,调用高层次的fill ()函数

​ 3.否则,采取最安全的方法,对于区间的每一个元素,一一调用construct函数进行构造

2.3.2 uninitialized_fill ()分析

参数:

​ 1.迭代器first:指向输入端起始位置

​ 2.n:表示欲初始化的空间大小

​ 3.x:表示初始值

实现流程:

​ 1.同样需要判断POD型别

​ 2.若是,则采取最有效的初值填写方法,调用高层次的fill ()函数

​ 3.否则,采取最安全的方法,对于区间的每一个元素,一一调用construct函数进行构造

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值