C++ | STL空间适配器

目录

一.内存池可能带来的问题

二.空间配置器code

三.set_new_handler(new_handler  new_p)

四.重定位new

五.空间配置器的分类


一.内存池可能带来的问题

我们知道容器的底层都是数据结构,如果我们在表层对一个容器频繁的进行插入或删除元素,也就是说在底层的数据结构上就会频繁的new和delete,这将会导致以下问题:

  1. 效率太低,系统要频繁的从用户态切换到内核态,这样不仅会费时,还会浪费许多资源。
  2. 造成内存碎片化。

 为了解决以上问题,我们可以使用一个内存池来处理,首先开辟一个大的内存单元,每次在插入元素时,就从内存池中选择一块内存来做元素的插入。删除元素的时候,将要删除元素所占的内存释放掉并归还给内存池,但是使用内存池来解决这类问题有一个缺点,如果内存池中的内存存放的是用户自定义类型的对象。

我们都知道对象的生成有以下两个步骤:

  1. 开辟内存
  2. 调用构造函数

对象的销毁有以下两个步骤:

  1. 调用析构函数
  2. 释放内存

 首先开辟一个大的内存空间,如果此时我们开辟了10个对象大小的内存空间。这时就会产生矛盾,如果现在我们只使用了1个内存空间来存放对象,也就意味着其他9个内存空间现在是空闲的,但这与我们上述的对象的生成步骤产生了矛盾。因为这块内存池在开辟10个对象大小的内存空间时,不仅开辟了内存还调用了构造函数,也就是说按理来说剩余的9个内存空间是存放在内存池中,只有需要时才会被分配,但此时已经调用了构造函数,导致后续如果有其他对象使用剩余的内存时就不能完整的生成对象了。

为了解决这个问题,我们将对象的生成的两个步骤分开执行,即

  1. 只开辟内存  allocate()
  2. 单独调用构造  construct()

对应的,将对象的销毁的两个步骤分开执行,即:

  1. 单独调用虚构  destory()
  2. 只释放内存   deallocate()

而空间适配器的工作原理就是将对象的生成和销毁的四个步骤单独的分离出来,让他们单独的执行。

二.空间配置器code

下面的程序是我自己实现的一个类似于库中的空间适配器,为了和库里的函数作区分,函数名均采用下划线+大写字母开头的形式。

#include<iostream>
#include<vector>

namespace CY130103/*空间作用域CY130101*/
{
	/*_Allocate只用来给对象开辟内存*/
	template<typename T>
	T* _Allocate(size_t size, T*) /*第一个形参size代表要开辟的内存个数,第二个形参只有类型没有名称*/
	{
		std::set_new_handler(NULL);/*set_new_handler   处理内存开辟失败的情况 */
		T* ptmp = (T*)::operator new(size * sizeof(T));

		if (ptmp == NULL)/*开辟失败*/
		{
			throw std::bad_alloc();/*抛出内存开辟失败的异常*/
		}
		return ptmp;
	}

	/*_Construct只用来调用对象的构造函数*/
	template<typename T>
	void _Construct(T* ptr)
	{
		::new (ptr) T();/*replacement new(重定位new)*/
	}

	/*_Destroy只用来调用对象的析构函数*/
	template<typename T>
	void _Destroy(T* ptr)
	{
		ptr->~T();//调用析构 
	}

	/*_Deallocate只用来释放对象的内存*/
	template<typename T>
	void _Deallocate(T* ptr)
	{
		::operator delete(ptr);
	}

	template<typename _Ty>
	class Allocator/*空间配置器的类*/
	{
	public:
		typedef _Ty value_type;//元素类型
		typedef _Ty* pointer;//指针类型
		typedef const _Ty* const_pointer;
		typedef _Ty& reference;//引用类型
		typedef const _Ty& const_reference;
		typedef size_t size_type;//元素大小
		typedef ptrdiff_t difference_type;

		template<typename U>
		struct rebind
		{
			typedef Allocator<U> other;
		};

		/*只开辟内存*/
		pointer allocate(size_type _Count)
		{
			return CY130103::_Allocate(_Count, (pointer)0);
		}

		/*只释放内存*/
		void deallocate(pointer ptr, size_t size)
		{
			CY130103::_Deallocate(ptr);
		}

		/*只调用构造*/
		void construct(pointer ptr)
		{
			CY130103::_Construct(ptr);
		}

		/*只调用析构*/
		void destroy(pointer ptr)
		{
			CY130103::_Destroy(ptr);
		}
	};
}
int main()
{

	std::vector<int, CY130103::Allocator<int>> vec;

	for (int i = 0; i < 10; i++)
	{
		vec.push_back(i + 1);
	}

	std::vector<int, CY130103::Allocator<int>>::iterator it = vec.begin();
	while (it != vec.end())
	{
		std::cout << *it << " ";
		it++;
	}
	std::cout << std::endl;

	return 0;
}

三.set_new_handler(new_handler  new_p)

set_new_handler(new_handler  new_p);

在只开辟内存的函数_Allocate()中,我们调用了上面这个函数,这个函数的具体用途是当operator new无法满足某一个内存分配的需求的时候,它会抛出一个异常,在这之前,它会先调用一个客户制定的错误处理函数new_handler。为了指定这个来处理内存不足的函数,使用者需要调用标准库程序函数set_new_handler。当operator new无法分配足够的内存时,set_new_handler函数会被调用,即set_new_handler是用来处理内存开辟失败的情况。

假如现在有一个进程要给某个变量开辟一块内存,但此时内存开辟失败了,系统并不会立即抛出内存开辟失败的异常,而是循环一段时间,不停的尝试去重新开辟内存,若此时有其他进程释放了内存,那么这个进程就又能成功的给这个变量开辟内存了。但如果在这一段循环的时间内还是无法成功的开辟内存,那么系统就会调用set_new_handler函数。

对于函数set_new_handler

函数说明

  • set_new_handler函数的作用是设置new_p指向的函数为new操作或new[]操作失败时调用的处理函数。
  • 设置的处理函数可以尝试使更多空间变为可分配状态,这样新一次的new操作就可能成功。当且仅当该函数成功获得更多可用空间它才会返回;否则它将抛出bad_alloc异常(或者继承该异常的子类)或者终止程序(例如调用abort或exit)。
  • 如果设置的处理函数返回了(例如,该函数成功获得了更多的可用空间),它可能将被反复调用,直到内存分配成功,或者它不再返回,或者被其它函数所替代。
  • 在尚未用set_new_handler设置处理函数,或者设置的处理函数为空(NULL)时,将调用默认的处理函数,该函数在内存分配失败时抛出bad_alloc异常。

参数说明

  • new_p:该函数指针所指的函数应为空参数列表且返回值类型为void
  • 该函数可以尝试获得更多的可用空间,或者抛出异常,或者终止程序。
  • 如果是一个空指针(NULL),处理函数将被重置为默认值(将会执行抛出bad_alloc异常)。

返回值

  • 返回先前被设置的处理函数指针;如果尚未被设置或者已被重置,将返回空指针。
  • 返回的函数指针是无参的void返回值类型的函数指针。

也就是说假如 set_new_handler函数的形参是一个返回值为void类型的函数A(),即set_new_handler(A),那么当operator new无法分配足够的内存时,set_new_handler函数会被调用,接着set_new_handler就会调用函数A()来处理内存开辟失败的情况。若set_new_handler的参数为NULL,那么set_new_handler函数被调用时就会调用bad_alloc(),来抛出内存开辟失败的异常。

四.重定位new

int a = 10;
char* c = new(&a) char('a'); 

 重定位new指的是,从new和类型之间所对应的内存单元中选择一块内存供外部使用。那么上述代码的意思就是从int类型的变量a中选择一块内存供char类型的变量a使用。

五.空间配置器的分类

  • 一级空间配置器
    • malloc
    • free

一级空间配置器就是将 malloc和free进行了封装,即在开辟内存时调用malloc,释放内存时调用free,没有内存池。如果开辟的内存>128个字节时,不容易产生内存碎片化的问题,这时系统会调用一级空间适配器。

  • 二级空间配置器
    • 内存池

二级空间配置器有内存池,在开辟内存和释放内存时会使用内存池。如果开辟的内存\leq128个字节时,容易产生内存碎片化的问题,这时系统会调用二级空间适配器。

二级空间配置器的内存池是一个自由链表内存池。开辟了一个长度固定的指针数组,这个指针数组的大小为16。

这个指针数组的每个单元格都指向一个自由链表,第一个自由链表的每个内存单元格的大小都是8,第二个自由链表的每个内存单元格的大小都是16,以此类推。然后根据要开辟内存的大小来选择合适的自由链表来存储数据。

发布了88 篇原创文章 · 获赞 40 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ThinPikachu/article/details/105185987