MyTinySTL阅读笔记---分配器

分配器

c++中的new和delete

1、new operator

new operator就是new操作符,平时使用的new(如int *pt = new object(...);)主要完成两个工作:

  1. 分配足够的内存以便容纳所需类型的对象(operator new)
  2. 调用构造函数初始化内存中的对象(placement new)

2、operator new

new操作符为分配内存所调用函数的名字是operator new,就是间接性的调用了 malloc函数。

源码:

void* __CRTDECL operator new(size_t const size)
{
    for (;;)
    {
        if (void* const block = malloc(size))
        {
            return block;
        }

        if (_callnewh(size) == 0)
        {
            if (size == SIZE_MAX)
            {
                __scrt_throw_std_bad_array_new_length();
            }
            else
            {
                __scrt_throw_std_bad_alloc();
            }
        }
    }
}

3、placement new

placement new用来在已经申请的内存上构建对象,其实不只是堆, 连栈上也能构建对象,这也是内存池经常用的方法。
尝试一波:

#include <iostream>

class test
{
private:
	int one;
public:
	test():one(9) {}
};

int main()
{
	char buffer[4] = { 0 };
	test *pt = new(buffer) test;

	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述

实际测试

#include <iostream>

class test
{
private:
	int one;
public:
	test():one(9) {}
};

int main()
{
	test *pt1 = new test;

	test *pt2 = (test*)operator new(sizeof(test));

	test *pt3 = new(pt2) test;

	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看见pt1(new operator)的效果就是pt2(operator new)+pt3(placement new)。

delete

delete的实现也是跟new有很多相似的, delete事先调用析构函数, 然后再调用free函数释放内存, 同样是可以将析构和释放内存分开调用, 也可以进行重载。

STL分配器

分配器将new的申请空间(allocate)和调用构造函数(construct)的两个功能分开实现。

基本的构造和析构工具

在这里插入图片描述

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

construct调用的是placement new, 在一个已经获得的内存里建立一个对象。

delete

destroy调用析构函数并且有两个版本
版本一传入指针直接就调用了析构函数:

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

版本二需要传入两个迭代器,并且根据是否有自定义的析构函数来执行析构:

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

template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) 
{
    // 分析是否有traival destructor(系统自带的析构函数)。
    typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
    __destroy_aux(first, last, trivial_destructor());
}

这是我们后面经常能看到的情况,根据对类的信息的判断做出相应的处理。

// 当__type_traits为__false_type时,通过迭代所有的对象并调用版本一的函数执行析构函数进行析构。
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) 
{
    for ( ; first < last; ++first)
        destroy(&*first);
}

// 当__type_traits为__true_type时, 什么也不做, 因为这样效率很高效, 并不需要执行析构函数。
template <class ForwardIterator> 
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {}

// 对字符指针的特化版本
inline void destory(char*, char*) {}
inline void destory(wchar_t*, wchar_t*) {}

allocate

allocate申请内存是有分为一级配置器(__malloc_alloc_template )和二级配置器(__default_alloc_template), 分配的空间小于128字节的就调用二级配置器, 大于就直接使用一级配置器, 一级配置器直接调用malloc申请, 二级使用内存池。

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)); }
};

在这里插入图片描述
在这里插入图片描述

一级配置器

一级配置器只是封装了mallocfreerealloc等c函数来进行内存的配置,并且在内存分配异常的时候可以执行用户指定的函数。

// 一级配置器
// inst完全没派上用场
template <int inst>
class __malloc_alloc_template 
{
// 这里private里面的函数都是在内存不足的时候进行调用的
private:
    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)();   // 内存不足设置的处理例程, 默认设置的是0, 表示没有设置处理例程, 这个处理例程是由用户手动设置的
#endif

public:
	static void * allocate(size_t n)
	{
	      void *result = malloc(n);	// 一级配置器直接使用malloc
	      if (0 == result) result = oom_malloc(n);	// 当内存不足的时候调用oom_malloc
	      return result;
	}
	
	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);	// 一级配置器直接使用realloc
	      if (0 == result) result = oom_realloc(p, new_sz);	// 当内存不足的时候调用oom_realloc
	      return result;
	}

	// 这里是模仿c++的set_new_handler. 是由用户自己定义的处理函数, 没有设置默认为0
	static void (* set_malloc_handler(void (*f)()))()
	{
	      void (* old)() = __malloc_alloc_oom_handler;
	      __malloc_alloc_oom_handler = f;
	      return(old);
	}
}

// 默认将处理例程设置为0, 只有用户自己设置
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

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);
	}
}

// 默认将malloc_alloc设为0
typedef __malloc_alloc_template<0> malloc_alloc;

二级配置器

因为比较小的内存会带来内存碎片等问题,并且也是一个内存负担。
二级配置器在分配超过128字节大小内存的时候使用一级配置器,否则使用内存池进行管理

内存池管理又称为层次配置:每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。16个空闲链表分别管理大小为8、16、24…120、128的数据块(为8的倍数方便管理)。

通过图片看一下:

  1. 下面是空闲链表中管理8byte的,链接了3块可用的内存,三个use的内存已经分给用户了,不归空闲链表管,其他空白区域就是未分配的内存。
    在这里插入图片描述

  2. 分配出去一个8byte的内存
    在这里插入图片描述

  3. 回收中间16byte
    在这里插入图片描述

enum {__ALIGN = 8}; // 设置对齐要求. 对齐为8字节, 没有8字节自动补齐
enum {__MAX_BYTES = 128};   // 第二级配置器的最大一次性申请大小, 大于128就直接调用第一级配置器
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  // 链表个数, 分别代表8, 16, 32....字节的链表

// 二级配置器
// threads用于多线程情况下不做讨论
// inst完全没派上用场
template <bool threads, int inst>
class __default_alloc_template
{
private:
	// 将bytes上调到8的倍数
	static size_t ROUND_UP(size_t bytes)
	{
	    return (((bytes) + ALIGN-1) & ~(__ALIGN - 1));
	}

	union obj
	{
	  union obj* free_list_link;  // 指向下一个区块
	  char client_data[1];        // 储存本块内存的首地址
	}

	// 16个freelist
	static obj * volatile free_list[__NFREELISTS];

	// 根据区块的大小决定使用第n个freelist n从1开始算起
	static size_t FREELIST_INDEX(size_t bytes)
	{
	    return (((bytes) + ALIGN-1) / __ALIGN - 1);
	}
	
	static void* refill(size_t n);
	static char* chunk_alloc(size_t size, int& nobjs);

	char *start_free;	// 内存池的首地址
	char *end_free;		// 内存池的结束地址
	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);
};

template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::obj * volatile size_t __default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

空间配置函数allocate检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。

static void * allocate(size_t n)
{
	obj * __VOLATILE * my_free_list;
	obj * __RESTRICT result;
	// 先判断申请的字节大小是不是大于128字节, 是则交给第一级配置器来处理. 否则继续往下执行
	if (n > (size_t) __MAX_BYTES) 
	{
		return(malloc_alloc::allocate(n));
	}
	// 找到分配的地址对齐后分配的是第几个大小的链表 获得该链表指向的首地址
	my_free_list = free_list + FREELIST_INDEX(n);
	result = *my_free_list;
	if (result == 0) 	// 没有多余的内存, 就先填充链表.
	{
		void *r = refill(ROUND_UP(n));
		return r;
	}
	// 返回链表的首地址, 和一块能容纳一个对象的内存, 并更新链表的首地址
	*my_free_list = result -> free_list_link;
	return (result);
};

空间释放函数deallocate根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。

static void deallocate(void *p, size_t n)
{
	obj *q = (obj *)p;
	obj * __VOLATILE * my_free_list;
	
	// 释放的内存大于128字节直接调用一级配置器进行释放
	if (n > (size_t) __MAX_BYTES) 
	{
		malloc_alloc::deallocate(p, n);
		return;
	}
	// 将内存直接还给对应大小的链表就行了, 并不用直接释放内存, 以便后面分配内存的时候使用
	my_free_list = free_list + FREELIST_INDEX(n);
	q -> free_list_link = *my_free_list;
	*my_free_list = q;
}

如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
  	int nobjs = 20;
  	char * chunk = chunk_alloc(n, nobjs); // 向内存池申请空间的起始地址 nobjs为引用
  	obj * __VOLATILE * my_free_list;
  	obj * result;
  	obj * current_obj, * next_obj;
  	int i;

  	// 如果只申请到一个对象的大小, 就直接返回一个内存的大小
  	if (1 == nobjs) return(chunk);
  	my_free_list = free_list + FREELIST_INDEX(n);

  	// 申请的大小不止一个对象的大小的时候
  	result = (obj *)chunk;
  	// my_free_list指向内存池返回的地址的下一个对齐后的地址
  	*my_free_list = next_obj = (obj *)(chunk + n);
  	// 这里从第二个开始的原因主要是第一块地址返回给了用户, 现在需要把从内存池里面分配的内存用链表给串起来
  	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);
}

从内存池取空间给空闲链表,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。

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 += total_bytes;  // 内存池的首地址往后移
	    return(result);
  	}
  	else if (bytes_left >= size) // 内存池的内存不足以马上分配那么多内存, 但是还能满足分配一个即以上的大小, 那就按对齐方式全部分配出去
  	{
	    nobjs = bytes_left/size;
    	total_bytes = size * nobjs;
    	result = start_free;
    	start_free += total_bytes;  // 内存池的首地址往后移
    	return(result);
  	} 
  	else // 如果一个对象的大小都已经提供不了了, 那就准备调用malloc申请两倍+额外大小的内存
  	{
	    size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
    	// 内存池还剩下的零头内存分给给其他能利用的链表, 也就是绝不浪费一点.
    	if (bytes_left > 0) 
    	{
      		// 寻找合适的freelist加入其中
      		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 = (char *)malloc(bytes_to_get);
    	// 内存不足了
    	if (0 == 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;
	        	if (0 != p) 
	            {
	            	// 找到未使用的区块 递归分配
	          		*my_free_list = p -> free_list_link;
          			start_free = (char *)p;
	          		end_free = start_free + i;
          			return(chunk_alloc(size, nobjs));
    	    	}
      		}
      		// 如果一点内存都没有了的话, 就只有调用一级配置器来申请内存了, 并且用户没有设置处理例程就抛出异常
      		end_free = 0;
      		start_free = (char *)malloc_alloc::allocate(bytes_to_get);
	    }
    	// 申请内存成功后重新修改内存起始地址和结束地址, 重新调用chunk_alloc分配内存
    	heap_size += bytes_to_get;
    	end_free = start_free + bytes_to_get;
   		return(chunk_alloc(size, nobjs));
  	}	
}

MyTinySTL分配器

涉及到的文件construct.h、allocator.h。
这边配置器取消掉了内存池,所以看起来就非常简单了。
有一些traits萃取剂相关内容会在迭代器中详细解释。

construct

构造直接调用了placement new

template <class Ty>
void construct(Ty* ptr)
{
	::new ((void*)ptr) Ty();
}

template <class Ty1, class Ty2>
void construct(Ty1* ptr, const Ty2& value)
{
	::new ((void*)ptr) Ty1(value);
}

template <class Ty, class... Args>
void construct(Ty* ptr, Args&&... args)
{
	::new ((void*)ptr) Ty(mystl::forward<Args>(args)...);
}

destroy

析构单个对象,这边调用了std::is_trivially_destructible用来判断是否有系统默认的析构函数,如果是系统默认的析构函数就没有必要执行。

template <class Ty>
void destroy(Ty* pointer)
{
	destroy_one(pointer, std::is_trivially_destructible<Ty>{});
}

/*! 普通可破坏类型 */
template <class Ty>
void destroy_one(Ty*, std::true_type) {}

/*! 有自定义的析构函数 */
template <class Ty>
void destroy_one(Ty* pointer, std::false_type)
{
	if (pointer != nullptr)
	{
		pointer->~Ty();
	}
}

析构两个迭代器之间全部对象

template <class ForwardIter>
void destroy(ForwardIter first, ForwardIter last)
{
	// 按照迭代器种类分类处理
	destroy_cat(first, last, std::is_trivially_destructible<typename iterator_traits<ForwardIter>::value_type>{});
}

/*! 普通可破坏类型 */
template <class ForwardIter>
void destroy_cat(ForwardIter, ForwardIter, std::true_type) {}

/*! 有自定义的析构函数 */
template <class ForwardIter>
void destroy_cat(ForwardIter first, ForwardIter last, std::false_type)
{
	for (; first != last; ++first)
	{
		destroy(&*first);
	}
}

allocate

分配器管理内存的分配、释放,对象的构造、析构,并且也知道数据的类型。

template <class T>
class allocator
{
public:
	typedef T            value_type;		/*! 数据类型 */
	typedef T*           pointer;			/*! 数据类型指针 */
	typedef const T*     const_pointer;		/*! const数据类型指针 */
	typedef T&           reference;			/*! 数据类型引用 */
	typedef const T&     const_reference;	/*! const数据类型引用 */
	typedef size_t       size_type;			/*! 数据类型大小 */
	typedef ptrdiff_t    difference_type;	/*! 数据类型指针距离 */

public:
	// 分配内存
	static T*   allocate();
	static T*   allocate(size_type n);

	// 回收内存
	static void deallocate(T* ptr);
	static void deallocate(T* ptr, size_type n);

	// 构造对象
	static void construct(T* ptr);
	static void construct(T* ptr, const T& value);
	static void construct(T* ptr, T&& value);

	template <class... Args>
	static void construct(T* ptr, Args&& ...args);

	// 析构对象
	static void destroy(T* ptr);
	static void destroy(T* first, T* last);
};

分配内存直接使用的operator new

template <class T>
T* allocator<T>::allocate()
{
	return static_cast<T*>(::operator new(sizeof(T)));
}

template <class T>
T* allocator<T>::allocate(size_type n)
{
	if (n == 0)
	{
		return nullptr;
	}
	return static_cast<T*>(::operator new(n * sizeof(T)));
}

回收内存使用的operator delete

template <class T>
void allocator<T>::deallocate(T* ptr)
{
	if (ptr == nullptr)
	{
		return;
	}
	::operator delete(ptr);
}

template <class T>
void allocator<T>::deallocate(T* ptr, size_type /*size*/)
{
	if (ptr == nullptr)
	{
		return;
	}
	::operator delete(ptr);
}

构造和析构对象直接调用的上面的construct和destroy,代码就不贴了。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值