STL第二章空间配置器的收货

一、placement new

new操作符不能被重载,假如A是一个类,那么A * a=new A;实际上执行如下3个过程。

  • 调用operator new分配内存,operator new (sizeof(A))
  • 调用构造函数生成类对象,A::A()
  • 返回相应指针

事实上,分配内存这一操作就是由operator new(size_t)来完成的,如果类A重载了operator new,那么将调用A::operator new(size_t ),否则调用全局::operator new(size_t ),后者由C++默认提供。
而new操作符有三种形式

void* operator new (std::size_t size);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
void* operator new (std::size_t size, void* ptr) throw(); 
  • 第一种分配size个字节的存储空间,并将对象类型进行内存对齐。如果失败返回NULL
  • 第二种在分配失败时会抛出异常。
  • 第三种是placement new版本,它本质上是对operator new的重载,定义于#include 中。它不分配内存,调用合适的构造函数在ptr所指的地方构造一个对象,并调用构造函数,之后返回实参指针ptr。

第一种和第二种函数是可以重载的,重载new运算符就是重载这两个函数。而第三种是不可以重载
而且第三种使用的格式也不一样,其形式如下

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializers list}

并且placement new可以给不是堆上的内存构造对象。程序如下

class A
{
public:
    A(int i) :a(i){}
    int getValue(){ return a; }
private:
    int a;
};

int main()
{
    A* p1 = new A(1);           //在堆上分配
    A  A2(2);                    //在栈上分配
    A* p2 = &A2;        
    cout << "placement new前的值: " << p1->getValue() << "  " << p2->getValue() << endl;

    A* p3 = new(p1) A(3);       //在p1的位置上构造
    A* p4 = new(p2) A(4);       //在p2的位置上构造
    cout << "placement new后的值: " << p1->getValue() << "  " << p2->getValue() << endl;
}

结果如下:
在这里插入图片描述
我认为就是在原来的内存位置上创建新的对象,如果原来有对象,就覆盖。使用placement new时一定要提前分好内存

二、第二章整体结构图

在这里插入图片描述
本章节主要介绍了这三个头文件中的内容,这是三个头文件的代码。

三、construct和destroy函数

其中construct主要是使用placement new函数给已经分配好的内存创造具体的类对象,并调用类构造函数来初始化
destroy则主要是调用类对象的析构函数。
所以要注意,这两个函数都没有配置内存空间或者删除内存的空间的功能,只是调用类对象的构造和析构函数。下面这个图说明了这两个函数的类型
在这里插入图片描述

四、stl_alloc头文件中的类关系图

在这里插入图片描述
simple_alloc类是封装好的给上层调用的接口,simple_alloc类中的函数调用的是以及配置器还是二级配置器,主要看__USE_MALLOC宏是否被定义。
具体关系如下:
在这里插入图片描述

五、__malloc_alloc_template(一级适配器)

第一级配置器的内存分配很简单,就是使用了C++标准自带的malloc,realloc分配内存,使用free释放内存。
但是有一个地方需要注意一下,就是如果malloc或者realloc分配内存时失败,那么就会采取new handler机制见书58页)。
该机制就是:在内存分配失败时,会先调用一个指定的函数,通常该函数被称为new_handler,但是这个new_handler函数一般是用户设置的,__malloc_alloc_template类中并没有定义这个函数的具体实现。如果这个函数也没有被客户端定义,那么出现内存分配失败就是直接抛出异常。

六、__default_alloc_template(二级配置器)基本数据结构

①空闲链表上的空闲块联合体
源码如下:

union _Obj {
        union _Obj* _M_free_list_link;//指向下一个空闲块的指针
        char _M_client_data[1];//空闲块的整体大小
  };

这里使用的联结体非常巧妙,因为联结体的特性就是联结体中的各个成员是公用内存空间的,由于这是空闲块,所以_M_client_data中的数据并没有用,只有_M_free_list_link指针中的值是有用的。所以不需要额外搞一块内存空间去存储指针,也不需要拆分空闲内存块,去容纳指针。在我自己写的内存分配器中,就是使用的在空闲块中分出一部分空间存储前驱指针和后继指针,指向一组的前一个空闲块和后一个空闲块。如果被占用,将指针位置直接覆盖成数据。操作比这个麻烦很多。
另外这个联结体还是柔性的,_M_client_data的大小并不一定是1,是随着你分配的内存有多大,就有多大,使用char类型只是因为一个char类型代表一个字节
下面以一个例子来具体说明这个结构体:

using namespace std;

union obj
{
	union obj* ptr;
	char data[1];
};

int main()
{
	obj *header = (obj *)malloc(10);//第一个内存块
	memset(header, 0, 10);//将内存清零是为了测试效果更好
	header->ptr = (obj *)malloc(20);//第二个内存块
	memset(header->ptr, 0, 20 );
	cout << header << endl;
	cout << header->ptr << endl;
	cout << _msize(header) << endl;//显示内存块中有多少内存空间
	cout << _msize(header->ptr) << endl;
	delete header->ptr;
	delete header;
	system("pause");
}

结果如图所示:
在这里插入图片描述
第一个obj块的内存图。ptr就是obj的第一个参数,可以看到就是存储的第二个obj块的地址
data就是obj的第二个参数
在这里插入图片描述
第二个obj块的内存图。
在这里插入图片描述
②空闲链表
源码:

static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];

相当于这个数组中每一个都是一个链表的头指针,类似①中示例程序的header,相当于总共有16条链表,每个链表的头指针都在这个数组中,并且数组中的每个头指针从0~15依次管理8/16/28/32/40…、128个字节的区域块的链表,所以空闲块尺寸只有8的整数倍,并且超过128字节的内存块就直接使用malloc申请,而不会去链表中寻找。
在我自己写的内存分配器中,也有空闲链表,但是每个节点依次管理{1}{2}{3 ~ 4}{5 ~ 8}…{1025 ~ 2048}大小的空闲块,所以我的空闲块尺寸允许是非8的整数倍,但由于字节对齐的缘故,所以是4的整数倍,并且所有大小的内存块都可以挂到空闲链表上。我觉得我这样内部内存碎片更小,但是可能效率比较低,可能外部碎片比较多。

③内存池
内存池其实就是malloc获取的内存,但是还没有放到空闲链表中去的,也就是有待使用的内存,只有等内存链表对应的内存块没有了,就会到内存池中申请,然后内存池切分出一些内存块挂到空闲链表上去。
源码:

  static char* _S_start_free;//内存池起始位置
  static char* _S_end_free;//内存池结束位置
  static size_t _S_heap_size;//记录堆得总大小,包括内存池和空闲链表上的空闲块以及正在使用的堆内存

最后用一张图来总结这三个数据结构之间的关系:
在这里插入图片描述

七、__default_alloc_template(二级配置器)具体操作函数

分配内存的流程图,以static void* allocate(size_t __n)函数为例
在这里插入图片描述
_S_chunk_alloc函数流程图
在这里插入图片描述
注意:obj空闲块从空闲链表上存取时,都是存入表头所指的第一个位置,或者取出表头所指的第一个位置。

我自己写的内存分配器,在内存分配中就没有这么复杂了,先寻找空闲链表中有没有适合的,如果没有就直接用malloc分配。没有考虑如果malloc失败应该如何处理

八、stl_uninitialized.h

这里面的函数主要用于填充和复制大块内存,其中有一个比较重要的概念就是POD,这种类型的数据必然有无用的构造,析构,拷贝构造和=操作符函数,所以可以直接采用逐字节拷贝的方法。所以在这些函数中会判断是否是POD类型。

九、静态函数

而且分配器中都是静态的变量以及函数,所以在一个源程序中,由一个分配器统一管理所有的stl容器的内存分配

十、内存释放过程

内存的释放过程比较简单,它接受两个参数,一个是指向要释放的内存块的指针p,另外一个表示要释放的内存块的大小n。分配器首先判断n,如果n>128bytes,则交由第一个分配器去处理,也就是用free函数直接释放;否则将该内存块加到相应的空闲链表中。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值