STL源码学习(1)-空间配置器

时间:2021-08-20 01:17:35


文章目录

  • 1. SGI空间配置器
  • 1.1 std::allocator
  • 1.2 std::alloc
  • 1.3 对象构造与析构:construct和destroy
  • 1.4 空间配置与释放:allocate和deallocate
  • 1.4.1 第一级空间配置器
  • 1.4.2 第二级空间配置器
  • 1.4.2.1 空间配置函数 allocate
  • 1.4.2.2 空间释放函数 deallocate
  • 1.4.2.3 重新填充 refill
  • 1.4.2.4 内存池 chunk_alloc
  • 1.4.2.5 操作举例
  • 1.4.2.6 关于内存池的一些问题
  • 1.5 内存基本操作函数
  • 1.5.1 uninitialized_copy
  • 1.5.2 uninitialized_fill
  • 1.5.3 uninitialized_fill_n

 

1. SGI空间配置器

SGI STL的空间配置器是 alloc而非allocator,并且不接受任何参数

vector<int,std::alloc> vec

我们通常使用缺省的空间配置器

template <typename T,typename Alloc=alloc>//指定其默认空间配置器为 alloc
class vector
{
	...
}

1.1 std::allocator

SGI的 allocator空间配置器 只是对 new 和delete 做了一层包装,效率及其不佳,不建议使用它

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) {
	    cerr << "out of memory" << endl; 
	    exit(1);
    }
    return tmp;
}


template <class T>
inline void deallocate(T* buffer) {
    ::operator delete(buffer);
}

allocate:包装了 new

deallocate:包装了 delete

因此 SGI的 allocator 是基层内存配置释放的行为,只有包装,没有效率的优势


1.2 std::alloc

SGI 重要的空间配置器:std::alloc

我们常写的c++语法:

class foo{};
class* obj=new class();
delete obj

使用new创建对象与使用delete销毁对象。

使用new时的完整操作:

  1. 首先调用 ::operator new配置内存
  2. 然后调用 构造函数 初始化对象。

同理delete的完整操作:

  1. 首先调用 析构函数 销毁对象。
  2. 然后调用 ::operator delete 释放内存

因此alloc把new和delete 以及他们各自的两部分分离开

  • alloc:allocate:配置内存
  • alloc:deallocate:释放内存
  • ::construct:构造对象
  • ::destroy:销毁对象

他们分别存在于这两个头文件中

#include <stl_alloc.h> //对象的配置与释放
#include <stl_construct>//对象的构造与析构

1.3 对象构造与析构:construct和destroy

来看 < stl_construct> 头文件中:

#include <new.h>  // 引入 new 运算符

最基本的两大函数:

template <class T>
inline void destroy(T* pointer) {
    pointer->~T();      // 析构
}

template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
  new (p) T1(value);    //T1::T1(value)
}

destroy直接负责某个对象的析构,即 ~T() 操作。

construct直接负责某个对象的创建,但是请注意这个new 是一个 placement new操作

形如: new (ptr) T(value) 是一个 placement new操作

括号里的参数ptr是一个指针,它指向一个内存缓冲器,placement new将在这个缓冲器上分配一个对象。
Placement new的返回值是这个被构造对象的地址(比如括号中的传递参数)。
placement new主要适用于:在对时间要求非常高的应用程序中,因为这些程序分配的时间是确定的;长时间运行而不被打断的程序;以及执行一个垃圾收集器 (garbage collector)。

比如:

int n=2;

new (&n) int(10);

std::cout<<n; // n = 10

其中这两个函数还具有其他的版本

//接受两个迭代器,并设法找出元素的类型 
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
  __destroy(first, last, value_type(first));
}

destroy的第二个版本:接受两个迭代器,然后 value_type会自动推导出 此迭代器的所指的元素的类型

value_type 属于__type_traits的成员,会自动推导出迭代器所指元素的类型,在往后的 第二章 我们会学到

然后根据推导出的第三个参数的类型选择 适当的 __destroy函数。

template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
//    trivial_destructor 是__type_traits<T>::has_trivial_destructor的别名
  typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  __destroy_aux(first, last, trivial_destructor());
}

思考一下:destroy有两个版本,第一个版本是一个对象的析构;第二个版本是 [first,last]范围 的析构

对于整个范围的析构:如果我们不知道这个范围有多大,则无论它是什么类型,我们都需要对范围的每一个元素执行析构函数,对于自定义类型:student,xxx等 具有我们显式定义的析构函数,我们必须这样做。

但是对于 int,char,long等内置类型,我们有必要对他们每次都调用析构函数(系统自带的)???

如果我们对 这些内置类型 进行一次又一次的析构,则是效率的巨大牺牲,因此有没有一种办法可以推断出即将析构的是内置类型还是自定义类型

  • 内置类型 _true_type类型 :直接什么也不做即可。
  • 非内置类型 _false_type 类型:执行每个元素的析构函数。

因此上面的destroy执行的就是这样的操作

对于判断它是什么类型的 value_type 的实现方法,我们在 迭代器一节 会给出方法。


如果是 内置类型 即**_true_type**类型:直接跳过即可

//无需析构
template <class ForwardIterator> 
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}

如果是 非内置类型 即**_false_type** 类型:执行析构函数

//需要析构destory
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  for ( ; first < last; ++first)
    destroy(&*first);
}

在false_type版本中执行的这个destroy 就是destroy的第一个版本,即直接调用

pointer->~T();      // 析构

1.4 空间配置与释放:allocate和deallocate

C++的内存配置与释放:new和delete

这两个函数相当于C的malloc和free。

因此SGI的空间配置与释放就是依据 malloc和free进行的

SGI 的双层级配置器:

  1. 第一级配置器用于 配置大小超过 128 字节的空间。
  2. 第二级配置器用于 配置大小小于 128 字节的空间。

第一级配置器:__malloc_alloc_template

__malloc_alloc_tetemplate <int inst>     //inst完全没用到
class __malloc_alloc_template 
{
    ...
}

第二级配置器:__default_alloc_template

template <bool threads, int inst>
class __default_alloc_template 
{
	...
}

无论是第一级还是第二级,SGI提供了一个接口,使得配置器的接口得以抽象

template<class T, class Alloc>
class simple_alloc {

public:
    static T *allocate(size_t n)
                { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }
    static T *allocate(void)
                { return (T*) Alloc::allocate(sizeof (T)); }
    static void deallocate(T *p, size_t n)
                { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
    static void deallocate(T *p)
                { Alloc::deallocate(p, sizeof (T)); }
};

注意:这个接口非常重要!

成员函数allocate和deallocate是可以选择调用 第一级还是第二级配置器 的实现方法的。

几乎所有的SGI STL容器全部使用这个接口:像是 vector,list等全部使用这个接口来处理空间的配置。

template <typename T,typename Alloc=alloc>
class vector
{
portected:
	typedef simple_alloc<T,Alloc> data_allocator	//数据空间配置器
	...
    void deallocate()
    {
        data_allocator::deallocate(xxxxx);
    }
}

deallocate函数 调用 data_allocator ,从而调用 deallocate函数,实际上就是 simple_alloc 的两个deallocate函数,然后再根据是第一级配置器还是第二级配置器选择哪个版本的 deallocate函数。

STL源码学习(1)-空间配置器

STL源码学习(1)-空间配置器

1.4.1 第一级空间配置器

我们从**__malloc_alloc_template**类 中 一层一层的看:

static void * oom_malloc(size_t);
static void *oom_realloc(void *, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
	static void (* __malloc_alloc_oom_handler)();
#endif

oom:out of memmory 表示内存不足。

__malloc_alloc_oom_handler是一个函数指针,可以处理内存不足的情况


创建n个大小的空间,allocate会直接 malloc这个大小的空间,如果分派失败,则调用内存不足的处理方法

static void * allocate(size_t n)
{
    void *result = malloc(n);   //第一级配置器直接使用malloc分配内存
    if (0 == result) result = oom_malloc(n);//无法满足要求,使用内存不足的处理方法
    return result;
}

释放某个对象的空间,直接使用 deallocate 的free

static void deallocate(void *p, size_t /* n */)
{
	free(p);    //直接使用free释放内存
}

扩容,使用 包装的ralloc即可,同样会处理内存不足的处理情况

static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
 {
     void * result = realloc(p, new_sz);
     if (0 == result) result = oom_realloc(p, new_sz);
     return result;
 }

发生内存不足的错误时的函数指针的处理:

可以指定 f 为 你任意的处理错误的方法,然后赋予给 函数指针。

//可以通过它指定你自己的out of memory 处理方法
//仿真 set_new_handle
static void (* set_malloc_handler(void (*f)()))()
{
    void (* old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = f;
    return(old);
}

当出现内存不足时的处理方法:

oom_malloc 和 oom_realloc 会尝试在例程中压缩出空间,如果挤出了一点内存则返回此内存,如果一点内存都挤不出来,则出发 失败的异常。

template <int inst>
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
    void (* my_malloc_handler)();
    void *result;

    for (;;) {//不断尝试
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }//失败直接抛出异常
        (*my_malloc_handler)(); //尝试调用例程,企图释放内存
        result = malloc(n);//尝试配置内存
        if (result) return(result);
    }
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
    void (* my_malloc_handler)();
    void *result;

    for (;;) {
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }//失败直接抛出异常
        (*my_malloc_handler)();
        result = realloc(p, n);
        if (result) return(result);
    }
}
#   define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)

失败直接退出


第一级空间配置器 利用 malloc free和ralloc实现出了对内存的分配与释放。

并且也实现了C++的 new handler机制

c++ 的new_handler机制: 当无法分配足够的内存时,在丢出std::bad_alloc之前,客端会调用一些指定的处理例程,称为 new_handler,然后尽全力帮你挤出内存,如果实在挤不出来则只能抛出异常。

但是 第一级空间配置器使用的是 malloc,而不是 new,所以无法实现 C++纯正的 set_new_handler机制,因此必须手动模拟一个 set_new_handler

正如上面的 oom_malloc 和 oom_ralloc 函数的实现

第一级空间配置器使用如下的别名:malloc_alloc

//malloc_alloc 为第一级空间配置器
typedef __malloc_alloc_template<0> malloc_alloc;

1.4.2 第二级空间配置器

__default_alloc_template 作为第二级配置器用来处理避免太多小额区块造成的运行负担。

区块越小,额外负担所占用的比例就越大,就越浪费

第二级配置器的做法:

  1. 如果区块大于 128 字节,则移交第一级配置器实现
  2. 如果区块小于 128 字节,则交由内存池管理,这种方法又叫做 次级配置,每次分配一大块内存,就会产生并且维护一个*链表
  1. 如果下次再有相同大小的区块需要分配,则直接从 free-list中取出
  2. 如果释放,则直接 回收 free-list 中的某一段

配置会自动将任何区块的内存需求量设置为 8 的倍数:30 -> 32

并且维护16块 free-list,每个块为8字节,每个块都是一个 free-list,free-list总大小即为128字节,每个free-list各自管理自己对应的 8个字节的小额区块

/*
      *链表 : free_list
*/
union obj {
    union obj * free_list_link;
    char client_data[1];    /* The client sees this.        */
};

使用 union 来节省内存空间

free-list 指向 还没有被分配的内存空间的地址,如果已经分配,则不再指向他们


预定义 区块及free-list的个数

enum {__ALIGN = 8};//上调边界
enum {__MAX_BYTES = 128};//小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  //free_list个数

此函数表示将区块的内存需求量都上升为 8 的倍数(每块总共有8个字节,表示成能分多少块

static size_t ROUND_UP(size_t bytes) {
            return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
      }

free-list的定义 : 是一个数组,数组的每一个元素都是一个指针。

根据区块大小,决定使用第几块 free-list

static obj * __VOLATILE free_list[__NFREELISTS]; //链表:指针数组
# endif
    //决定使用第几号 free_list  从1开始
  static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

数据成员:

// Chunk allocation state.
static char *start_free;  //内存池起始位置
static char *end_free;    //内存池结束位置
static size_t heap_size;

1.4.2.1 空间配置函数 allocate

allocate的执行过程:

  1. 如果需要分配的空间 n超过了128个字节的大小,则就转到第一级空间配置器的实现上。
  2. 然后再从16个 free-list中选择适当的一个。
  3. 如果free-list中有可用的区块,则直接拿来用;否则就把区块的大小上调至 8 的倍数,然后再refill重新填充 free-list。
  4. 然后调整 free-list 为选出第 n 个区块后的下一个free-list。
  5. 最后取出 result 指向的第 n 个区块的空间起始地址
/*
    空间配置函数
 */
static void * allocate(size_t n)
{
    obj ** my_free_list;//二级指针
    obj * result;
    //大于128 就调用第一级配置器
    if (n > (size_t) __MAX_BYTES) {
        return(malloc_alloc::allocate(n));
    }
    //从16个 free_list中选择一个使用
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;//result指向当前选择的*链表
    if (result == 0) {
        void *r = refill(ROUND_UP(n));//free_list全为空, 准备重新填充free-list
        return r; //返回起始地址
    }
    //取出了 result这一块, 因此 free-list往后连接,跳过result这一块
    *my_free_list = result -> free_list_link;
    return (result);//取出该块起始地址
};

1.4.2.2 空间释放函数 deallocate

回收区块:

  1. 如果回收大于 128 个字节,则转到第一级空间配置器
  2. 寻找对应的 free-list区块
  3. 把 p 指向的某一个区块插入到free-list中
  1. 首先q的next地址为free-list对应的下一个区块
  2. free-list的当前区块转为 q区块
  3. 其实就是完成了链表的中间插入
//释放空间
static void deallocate(void *p, size_t n)
{
    obj *q = (obj *)p;
    obj ** my_free_list;

    //大于128调用第一级配置器
    if (n > (size_t) __MAX_BYTES) {
        malloc_alloc::deallocate(p, n);//释放从p开始的 n 个元素的空间
        return;
    }
    my_free_list = free_list + FREELIST_INDEX(n);//寻找对应的free_list
    //调整list 回收区块
    q -> free_list_link = *my_free_list;
    *my_free_list = q;
}

1.4.2.3 重新填充 refill

前面讲到从free-list中取出第 n 个区块的区块,万一free-list中已经没有了可以用的区块,则需要重新填充 free-list *链表

  1. 从内存池中取出nobjs个区块,作为free-list的新节点,nobjs是引用的方式传递的。
  2. 如果只获得了一个区块,则直接分配给调用者用,free-list无需新的区块,通过返回 chunk 给到allocate的返回值;否则准备调整free-list,纳入新的节点。
  3. 然后在 chunk 空间内建立 free-list
/*
返回一个大小为n的对象,并且为free_list增加节点
*/
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;
    //nobjs通过传递引用的方式 来取得nobjs个区块作为free_list的新节点
    char * chunk = chunk_alloc(n, nobjs);

    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;

    if (1 == nobjs) return(chunk);//只获得一个区块,直接返回给调用者
    //多个区块:调整free_list  纳入新节点
    my_free_list = free_list + FREELIST_INDEX(n);

      //在chunk空间里建立free_List 
      result = (obj *)chunk;
      *my_free_list = next_obj = (obj *)(chunk + n);//free_list指向新配置的空间(取自内存池)

      //将free_list各个节点串联起来
      for (i = 1; ; i++) {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);
        if (nobjs - 1 == i) {
            current_obj -> free_list_link = 0;
            break;
        } else {
            current_obj -> free_list_link = next_obj;
        }
      }
    return(result);
}

1.4.2.4 内存池 chunk_alloc

  1. 我们的内存池一定会尝试拥有一个 大小为单个节点空间的 20倍的一个内存空间(所以才叫做内存池)
  2. 如果内存池的剩余空间大于这20倍的空间,则内存池容量充足,直接全部保存下来
  3. 如果内存池的剩余空间只能容纳小于20个,但是大于1个空间。则内存池会计算出最大的块数与最大能用的空间,把这剩余的空间全部保存下来
  4. 如果一个节点空间也容纳不了。则尝试从heap堆中取得空间,则需要从堆中申请 2倍的 20倍的节点的空间+堆的额外空间
  5. 最后调整堆空间,递归调用该函数,直到获取到了空间或者抛出异常为止。
//内存池
	static char* chunk_alloc(size_t size, int& nobjs)
	{
		char* result;
		size_t total_bytes = size * nobjs;//总的需要分配的大小  *20倍
		size_t pool_bytes_left = end_free - start_free;//内存池剩余空间

		if (pool_bytes_left >= total_bytes) {
			//内存池剩余容量充足 可以容纳20个节点
			result = start_free;//起始
			start_free += total_bytes;//total_bytes全部分配
			return(result);
		}
		else if (pool_bytes_left >= size) {
			//内存池剩余容量不能容纳全部,但是可以容纳 1个 及以上的块的空间
			//nobjs是引用类型,修改为实际能够供应的区块数
			nobjs = pool_bytes_left / size;//最大能容纳的n
			total_bytes = size * nobjs;//最大能分配的空间
			result = start_free;//起始地址空间
			start_free += total_bytes;//最多能容纳的total_bytes全部分配
			return(result);
		}
		else {
			//一个块都容纳不了! 则尝试heap中配置
			//新的空间大小为原始需要分配的空间大小的两倍

			//从heap上分配的空间大小: 2*所需节点空间大小*20+额外空间
			size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
			//内存池还有一点剩余,但是不够一个节点的空间
			if (pool_bytes_left > 0) {
				Area* volatile* my_free_list = free_list + FREELIST_INDEX(pool_bytes_left);

				//将内存池中的剩余空间全部编入
				((Area*)start_free)->next = *my_free_list;
				*my_free_list = (Area*)start_free;
			}

			start_free = (char*)malloc(bytes_to_get);
			if (0 == start_free) {//malloc失败,说明heap上没有足够空间分配给我们了
				Area* volatile * my_free_list, * p;
				for (int i = size; i <= (size_t)__MAX_BYTES; i += (size_t)__ALIGN) {
					my_free_list = free_list + FREELIST_INDEX(i);
					p = *my_free_list;
					if (0 != p) {
						*my_free_list = p->next;
						start_free = (char*)p;
						end_free = start_free + i;
						//递归调用自己,修复nobjs
						return(chunk_alloc(size, nobjs));
					}
				}
				/*
				一点内存都没有了
				*/
				end_free = 0;	// In case of exception.
				//最后调用第一级配置器,看看能否得到改善
				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));
		}
	}

1.4.2.5 操作举例

我们直接拿第二级空间配置器alloc来举一个例子

alloc all;
int* b = (int*)all.allocate(1);
*b = 10;
all.deallocate(b, 1);
int* c = (int*)all.allocate(1);
all.deallocate(c, 1);
delete b;

我们从最上面开始执行,它会如何操作呢?

首先执行:int* b = (int*)all.allocate(1);进入allocate函数

要分配一个元素的空间,但是初始的时候肯定*链表是NULL的,所以首先一定会直接进入 refill 函数里,填充free-list

进入 refill函数:refill函数作用:取出内存并且填充free-list空间

  1. refill直接进入 memory内存池中 尝试取得内存空间,进入 chunk_alloc函数:
  1. 总的需要分配的大小为所需要的单个节点的20倍(单个节点的对齐为8),因此total_bytes=160,但是v第一次内存池的空间为0,所以会进入else里面从heap中尝试开辟空间。
  2. heap中需要开辟的空间为total_bytes的两倍再加上堆的额外空间,使用malloc分配内存,赋值给start_free
  3. 分配成功,分配给heap空间,调整start_free与end_free,然后进入递归。
  4. 再次回到 chunk_alloc 函数中,此时内存池的空间为 320,足够所需要的160,因此赋值result为当前可用空间的地址,返回result。
  1. 回到refill 中,chunk表示内存池的首地址。首先result表示地址空间的首地址,即取出一块8字节(内存对齐规则),表示我们将要取出的,然后再把剩下的19块填充到 *链表 中
  2. 然后从第二块开始进行块的尾插入链表,我们之前取出的result直接返回即可。
  3. 回到 allocate函数里,返回的 r 就是我们的空间的首地址,返回即可。

然后我们准备销毁b的空间:

  1. 进入deallocate函数,寻找对应的free-list块为第0块
  2. 然后把q块头插入为*链表的头,相当于完成了q所指的块空间的回收,*链表又有了20个块。

然后我们打算对c分配空间:

  1. 进入allocate函数,发现*链表存在空余的块空间,因此无需进入refill重新填充
  2. result获得当前能够使用的块空间的首地址,直接返回即可。
  3. 然后*链表失去了这个块,因此跳过这个块,重新调整*链表的指向。

同理我们销毁c的空间:直接进入deallocate函数,然后把c所指向的块回收,然后*链表重新获得了此块的空间。

最后我们delete整个b,因为这个内存池是由b创建的,因此我们销毁delete b之后的,b所指向的空间就会被销毁,恰好b的空间为第一个*链表的所有空间。

1.4.2.6 关于内存池的一些问题

STL内存配置器有没有内存泄漏?

看了源码后很多人疑惑为什么在该Allocator的实现里只有对内存池malloc的代码,没看到类似free这样释放内存的代码,甚至该Allocator类都没有析构函数,这样不是会存在内存泄漏吗?

其实不然。

对于由链表维护的内存,其内存的释放工作应该是上一层调用者负责,比如容器Vector在析构函数中就将其申请的所有capacity大小的内存释放。相反内存池的中的内存将会一直保留直到程序退出。有的同学可能会认为“这不就是内存泄漏吗?比如创建了一个Vector变量,到Vector析构了之后再内存中竟然有一块内存没有被系统回收,这不是memory leak吗”。其实不然:

  1. 申请的内存没有被及时释放 不等于 内存泄漏

在单线程中,由于该Allocator中记录内存池起始的指针是static静态类型,所以只要是你在同一个线程中,无论你创建多少个Allocator,记录内存池的变量都是同一个,换句话说,当下次再创建Vector时,还是使用上一次使用的那个。也就是说他的存在时有意义的,这也是cache或memory pool的意义所在!

  1. 该内存池不会疯狂野生长

这个内存池的空间其实是很小的,因为大于128Byte的内存申请都直接转调了malloc,从源码中也可以看出,内存池每次重新灌水的新容量是2*total_size + round_up(heap_size >> 4)。

内存池的存在是为了避免大量内存碎片的产生,代价是管理内存所需要多付出的时间和空间消耗。

以上就是内存池一种存在直至程序退出的原因。


1.5 内存基本操作函数

STL有五个非常重要的全局函数:

  1. 用于构造的construct函数
  2. 用于析构的destroy函数
  3. uninitialized_copy :copy
  4. uninitialized_fill:fill
  5. uninitialized_fill_n:fill_n

前两个已经在对象的构造与析构中讲过了

另外的三个函数将在本节讲解。


1.5.1 uninitialized_copy

uninitialized_copy函数能够使得内存的配置对象的构造分离开。

template <class InputIterator, class ForwardIterator>
inline ForwardIterator
  uninitialized_copy(InputIterator first, InputIterator last,
                     ForwardIterator result) {
  return __uninitialized_copy(first, last, result, value_type(result));
}

将 first到 last 迭代器范围的元素拷贝到 result所指向的位置。

即 该函数将 [first,last)范围内的元素 复制一份,并把这些 复制品放入 result的空间里。

针对输入范围的每一个迭代器 i ,construct 都会执行如下操作:

  • new (ptr) T(val)
  • constuct(&*(result+(i-first)),*i),产生 *i 的复制品,放置于输出位置 result后的相对位置中

在实现任何一个容器的构造函数的时候,通常会有两个步骤:

  1. 配置内存空间的大小
  2. 使用 uninitialized_copy来在该内存区块上构造元素

uninitialized_copy 中调用的函数:

该函数 使用__type_traits 操作来推断出迭代器所指的元素类型是不是POD类型

  • 如果是 POD __true_type:则拷贝时执行最简单的内存拷贝
  • 如果是 非POD __false_type:则需要调用相应的构造函数来逐一拷贝
template <class InputIterator, class ForwardIterator, class T>
inline ForwardIterator
__uninitialized_copy(InputIterator first, InputIterator last,
                     ForwardIterator result, T*) {
  typedef typename __type_traits<T>::is_POD_type is_POD;
  return __uninitialized_copy_aux(first, last, result, is_POD());
}
template <class InputIterator, class ForwardIterator>
inline ForwardIterator 
__uninitialized_copy_aux(InputIterator first, InputIterator last,
                         ForwardIterator result,
                         __true_type) {
  return copy(first, last, result);//是POD类型
}

template <class InputIterator, class ForwardIterator>
ForwardIterator 
__uninitialized_copy_aux(InputIterator first, InputIterator last,
                         ForwardIterator result,
                         __false_type) {
  ForwardIterator cur = result;//不是POD类型
  __STL_TRY {
    for ( ; first != last; ++first, ++cur)
      construct(&*cur, *first);//逐一进行拷贝
    return cur;
  }
  __STL_UNWIND(destroy(result, cur));
}

1.5.2 uninitialized_fill

他的作用是为指定范围的元素赋予一个值:[first,last) 赋予初始值 val

与 uninitialized_copy类似,如果迭代器范围的每一个元素都指向一个未知的区域,那么会对迭代器范围的每一个元素都调用一次: construct(&*i,val),然后在这个位置上产生一个值val

同样使用value_type推断出迭代器所指元素的类型,然后传入__uninitialized_fill。

template <class ForwardIterator, class T>
inline void uninitialized_fill(ForwardIterator first, ForwardIterator last, 
                               const T& x) {
  __uninitialized_fill(first, last, x, value_type(first));
}

在__uninitialized_fill函数内部会使用 __type_traits推断类型是否为POD类型。

  • 如果是POD类型,则直接采用最简单的fill 填充的方式(STL算法)
  • 如果不是POD类型,则对每一个元素调用其构造函数

使用 first!= last ,first++ 作为for循环的参数

template <class ForwardIterator, class T>
inline void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, 
                         const T& x, __true_type)
{
  fill(first, last, x);
}

template <class ForwardIterator, class T>
void
__uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, 
                         const T& x, __false_type)
{
  ForwardIterator cur = first;
  __STL_TRY {
    for ( ; cur != last; ++cur)
      construct(&*cur, x);
  }
  __STL_UNWIND(destroy(first, cur));
}

template <class ForwardIterator, class T, class T1>
inline void __uninitialized_fill(ForwardIterator first, ForwardIterator last, 
                                 const T& x, T1*) {
  typedef typename __type_traits<T1>::is_POD_type is_POD;
  __uninitialized_fill_aux(first, last, x, is_POD());               
}

另外 fill 的实现非常简单:其实就是往迭代器的范围赋值而已

template <class ForwardIterator, class T>
void fill(ForwardIterator first, ForwardIterator last, const T& value) {
  for ( ; first != last; ++first)
    *first = value;
}

1.5.3 uninitialized_fill_n

该函数接受三个参数,一个 first 表示起始位置;一个 n,表示操作的数量;一个val。

从first迭代器处开始,操作 n 个地址空间,把val赋值给这些位置。

其实与上面的uninitialized_fill是一样的,只不过上面表示范围,这个表示一个数量

template <class ForwardIterator, class Size, class T>
inline ForwardIterator uninitialized_fill_n(ForwardIterator first, Size n,
                                            const T& x) {
  return __uninitialized_fill_n(first, n, x, value_type(first));
}

同理使用 value_type 推断出迭代器的元素类型,然后再判断其是否为POD类型;

  • 如果是,则直接 fill_n 即可
  • 如果不是,则需要调用其 构造函数

使用 n-- ,当n=0时停止,这个for来作为循环。


注意:上面的三个函数都具有一个特性:commit or rallback

即要么对范围的操作全部成功(全部赋值成功),要么全部不成功

如果不成功,会 抛出异常,然后调用destroy,将全部的成功的元素析构掉,因为他已经失败了,不允许他一半成功,一半失败

destroy(result, cur);//uninitialized_copy
destroy(first, cur);//uninitialized_fill
destroy(first, cur);//uninitialized_fill_n

destroy 接受一个范围,然后就是我们上面所讨论的destroy的内容了


STL源码学习(1)-空间配置器