C++STL学习总结系列之【1.空间配置器】

目录

1.空间配置器

what

why

how

2.特殊空间配置器(std::alloc)

what

why

how

3.一级空间配置器

what

why

how

4.二级空间配置器

what

why

how

5.内存池

what

why

how

6.自由链表

what

why

how

7.总结


本文只总结了空间配置器的一些考点作为初学者了解用,具体实现细节请参考文末附加的链接。

1.空间配置器

  • what

空间配置器,顾名思义,就是为各个容器高效管理空间(空间的申请与回收)的,在默默地工作。

虽然在常规使用STL时,可能用不到它,但站在学习研究的角度,学习它的实现原理对我们有很大的帮助。

  • why

当我们自己动手实现vector、list、map、unordered_map等容器时,所有需要的内存空间都是通过malloc或new申请的,虽然代码可以正常运行,但是有以下不足之处:

  1. 内存泄漏:堆区空间申请与释放需要程序员自己管理;
  2. 内存碎片:频繁申请小块内存会产生外部碎片(注意区分内部碎片与外部碎片);
  3. 效率低:频繁申请小块内存;
  4. 代码结构混乱、复用性不高;

因此,需要设计一个高效的内存管理机制,在STL中该模块被称之为空间配置器。

  • how

SGI-STL(硅图公司)空间配置器的原理:

通常来讲,C++内存分配与释放使用new和delete。

对于new,是先分配内存,后调用构造函数;而对于delete,是先调用析构函数,后释放内存。

SGI-STL空间配置器将这一过程设计为四个操作:内存配置(alloc::allocate)、内存释放(alloc::deallocate)、对象构造(alloc::construct)、内存释放(alloc::destroy)。

本文主要总结内存配置与释放的模块。

SGI-STL空间配置器分为标准空间配置器(std::allocator)以及特殊空间配置器(std::alloc)

标准空间配置器(std::allocator)只是对new和delete的浅层封装,实用价值较低,在SGI-STL中也从未使用过。

通常使用的是特殊空间配置器(std::alloc),该空间配置器采用两级结构

2.特殊空间配置器(std::alloc)

  • what

SGI-STL以128字节作为小块内存与大块内存的分界线,设计了具有两级结构的特殊空间配置器。

  • why

上述提到的不足之处,究其原因是由于频繁使用malloc和free向系统申请小块内存造成的,因此设计具有两级结构的特殊空间配置器来解决上述问题。

  • how

>128字节的内存使用一级空间配置器,<=128字节的内存使用二级空间配置器。

3.一级空间配置器

  • what

用于分配>128字节内存大小的空间。

  • why

为了更大程度合理运用空间,压榨剩余内存,达到内存的高效使用,尽量开辟想要的内存空间(在原理中会讲到为什么能压榨剩余内存)。

  • how

直接调用malloc和free(而不是new和delete,前文提到了,在空间配置器中分别具有内存配置、内存释放、对象构造、内存释放四个操作)。

该设计的两个关键之处:在Oom_Malloc中不断循环申请空间(上文提到的压榨剩余内存的原因)、new_handler机制

先看一眼整体的设计逻辑:

一级空间配置器通过malloc()申请空间,而释放空间直接调用free(),当内存不足申请失败时,调用函数Oom_Malloc(Oom=Out of memory)

在Oom_Malloc中,模拟C++的new_handler机制,通过一个函数指针指向指定的处理函数

所谓C++ new handler 机制就是:

你可以要求系统在内存配置需求无法被满足的时候,调用一个你指定的函数,

也就是说,一旦 ::operator new 无法完成任务,在丢出异常之前,会先调用客户端指定的处理函数,该处理函数通常被称为 new-hanler。

如果不存在自定义该处理函数,则抛出异常,通过exit(1)的方式退出;

如果存在自定义的处理函数,则调用该处理函数申请内存空间。

Oom_Malloc函数只有两个出口点:1.不存在自定义处理函数,抛出异常;2.申请到空间,返回并退出。

可以看出,在定义了指定的处理函数的情况下,如果没有申请到对应大小的内存,则会陷入死循环,等待有对应大小的内存直到申请成功。

这一设计使得内存能够得到高效的使用。

以下是截取的部分关键代码:

#pragma once
#include<cstdlib>  //malloc、free的头文件
#include<iostream>

/*__malloc_alloc_template*/
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;

//定义的抛出异常的宏,通过exit(1)的方式退出
#define __THROW_BAD_ALLOC std::cerr<<"out of memory"<< std::endl; exit(1)

class OneSpce
{
private:
	//以下是函数指针,所代表的函数将用来处理内存不足的情况
	//oom(out of memory)
	static void *Oom_Malloc(size_t);
	static void *Oom_Realloc(void *, size_t);
	static void(*__malloc_alloc_oom_handler)();
public:
	// 对malloc的封装
	static void * Allocate(size_t n)
	{
		//一级空间配置器直接使用malloc(),申请空间成功,直接返回,失败则交由oom_malloc函数处理
		void *result = malloc(n);
		//内存空间申请失败时,调用 oom_malloc() 
		if (0 == result)
			result = Oom_Malloc(n);
		return result;
	}

	// 对free的封装 这个size_t完全可以不要,但是为了统一接口,加上了这个size_t
	static void Deallocate(void *p, size_t /* n */)
	{
        //一级空间配置器直接使用free()
		free(p);  
	}

	// 模拟set_new_handle
	// 该函数的参数为函数指针,返回值类型也为函数指针
	// void (* set_malloc_handler( void (*f)() ) )()
	typedef void(*PFUNC)();
	static PFUNC set_malloc_handler(PFUNC f)

	static void(*set_malloc_handler(void(*f)()))()
	{
		void(*old)() = __malloc_alloc_oom_handler;
		__malloc_alloc_oom_handler = f;
		return(old);
	}
};

//如果没有设置的话,在下面的函数中就只能抛出异常
void(*OneSpce::__malloc_alloc_oom_handler)() = 0;  


// malloc申请空间失败时改用该函数
void * OneSpce::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);
	}
}

4.二级空间配置器

  • what

用于分配<=128字节内存大小的空间。

  • why

为了避免频繁申请小块内存的操作造成的效率低、内存碎片的问题,并提高用户获取空间的速度与高效管理,减小外部碎片的生成。

  • how

内存池+自由链表的设计。

通过预先申请大块内存作为内存池,随后将内存池中的内存分块挂载到自由链表上,需要内存从自由链表上取,不再使用的内存放回内存池重新挂载到自由链表上,并未释放。

5.内存池

  • what

顾名思义,它是预先申请的一大块内存,具有三个参数:内存起始地址、终止地址以及内存池大小(=终止地址-起始地址)。

在这里插入图片描述

  • why

为了避免频繁申请小块内存的操作造成的效率低、内存碎片的问题。

  • how

第一次申请小内存时,先申请一大块内存留作备用,之后申请小内存时,直接从已经申请的一大块内存中划去,不再向系统申请,当内存池空间不足时,再从堆区申请空间。

6.自由链表

  • what

实际上是哈希桶结构(拉链法解决哈希冲突)。

在这里插入图片描述

  • why

为了提高用户获取空间的速度与高效管理,减小外部碎片的生成。

  • how

将内存池中内存划分为8的倍数大小的区块,分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节共16个大小类型,挂接到free_list中,

并主动将任何小额区块的内存需求量上调到8的整数倍(如申请25字节的内存,则分配32字节内存),

若有相同大小的内存需求,直接从free_list中取,如果客户端释放小额区块,就由配置器回收到 free_lists 中。

7.总结

申请一块n字节的内存的逻辑:

n>128 调用一级空间配置器,n<=128 调用二级空间配置器。

附逻辑图:

空间配置器解决的问题:

1.通过内存池解决了频繁使用malloc、free开辟和释放小内存带来的性能效率低下;

2.通过free_list设计分配8字节倍数大小内存优化外部碎片问题,但并没有完全解决内存碎片的问题(只能优化,并不能完全不产生碎片)。

带来的问题:

1.没有释放自由链表free_list所挂区块的函数,释放时机是程序结束,导致自由链表一直占用内存,自己进程可以使用,其他进程却用不了。

附参考链接(含部分源码):

https://blog.csdn.net/qq_40421919/article/details/89192383

https://blog.csdn.net/hujian_/article/details/51063935?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

https://blog.csdn.net/xy913741894/article/details/66974004

https://blog.csdn.net/qq_37299596/article/details/106952846

猜你喜欢

转载自blog.csdn.net/qq_37348221/article/details/112982289