文章目录
一、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函数直接释放;否则将该内存块加到相应的空闲链表中。