针对一个class写出它的内存管理池以及总结出allocator类(三个版本)

如果我们不针对对象做内存管理,那么我们每次进行Foo* p = new Foo(x);时总是会调用malloc函数。
尽管malloc函数很快,但是我们仍然有自己管理内存的必要。
https://blog.csdn.net/qq_42604176/article/details/111234556中曾经记录到有些操作系统多次调用malloc函数会把原来很大的一块连续内存区域逐渐地分割成许多非常小而且彼此不相邻的内存区域,也就是内存碎片。这也是一种隐患。
我们可以首先使用malloc函数申请一大段内存,然后切割成若干个小块,每次创建一个对象的时候就给一小块的内存。这样效率更高并且更容易管理。
并且如果没有经过特殊设计,一次创建对象就调用一次malloc,一次malloc就会得到两个cookie(8个字节)。创建多个对象的时候这样就会比较浪费空间。
内存池设计的目的就是要提高速度和降低浪费。

示例版本1:per-class allocator,1

下面是示例:

#include<cstddef>
#include<iostream>
using namespace std;

class Screen {
public:
	Screen(int x):i(x) {};
	int get() {return i;}
	
	void* operator new(size_t);
	void operator delete(void*,size_t);
	//...
private:
	Screen* next;
	static Screen* freeStore;
	static const int screenChunk;
private:
	int i;
};
Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;

出于对内存管理的考虑,我们需要挖一大块内存,并且内存里面的小块需要用指针联系到一起。所以可以看到上面class内部会有一个next指针,指向Screen类型的对象。但是这样会有一个遗憾,因为多出来一个指针,导致空间又浪费掉了。
针对一个class写出它的内存管理池以及总结出allocator类(三个版本)
接下来是函数的重载。核心的操作步骤就是单向链表的操作。将空闲的内存做成链表,每次开辟新内存的话,就将链表中的一个分配出去。

void* Screen::operator new(size_t size)
{
	Screen *p;
	if(!freeStore) {
		//linked list 是空的,所以申请了一大块内存
		size_t chunk = screenChunk * size;		//一次挖24个对象的内存
		//将指针转型
		freeStore = p = reinterpret_cast<Screen*>(new char[chunk]);
		//将一大块分割成小块,当作linked list串接起来
		for(;p != &freeStore[screenChunk - 1]; ++p)
			p->next = p + 1;
		p->next = 0;
	}
	p = freeStore;
	freeStore = freeStore->next;
	return p;
}

如果回收的话,析构对象,将内存重新放置回链表中:
注意这里是将回收的内存指针放在链表头部,因为这样操作比较快。只需要动头指针就行了。注意这里的内存并没有还给操作系统,而是将使用过的内存穿成一个链表。

void Screen::operator delete(void *p,size_t)
{
	//将delete object 插回free list前端
	(static_cast<Screen*>(p))->next = freeStore;
	freeStore = static_cast<Screen*>(p);
}

测试函数:


cout << sizeof(Screen) << endl;		//16

size_t const N = 100;
Screen* p[N];

for(int i = 0; i < N; ++i)
	p[i] = new Screen(i);

//输出前10个pointers,比较其间隔:
for(int i = 0; i < 10; ++i)
	cout << p[i] << endl;

for(int i = 0; i < N; i++)
	delete p[i];

效果:
可以看出每个类之间的内存间隔是16,确实没有cookie的内存。

16
00000280B9B21CF0
00000280B9B21D00
00000280B9B21D10
00000280B9B21D20
00000280B9B21D30
00000280B9B21D40
00000280B9B21D50
00000280B9B21D60
00000280B9B21D70
00000280B9B21D80

如果我们不对两个函数进行重载,得到的结果如下,由于电脑操作系统是多进程的,所以可能在内存分配的时候有其他任务执行,导致内存不是连续的,估摸着间隔应该是5*16。

16
000001BA32211920
000001BA322120A0
000001BA32211A10
000001BA32211970
000001BA32211C90
000001BA32212460
000001BA322124B0
000001BA32212500
000001BA32211EC0
000001BA322119C0

在侯捷老师的PPT上是VC6、GNU环境下的,结果如下:
针对一个class写出它的内存管理池以及总结出allocator类(三个版本)

示例版本2:per-class allocator,2

这个版本主要是利用union对第一版本的指针进行优化。
关于union这里做一个简单回顾,毕竟基本没用过这东西。
具体细节可以参考:https://blog.csdn.net/yuyanggo/article/details/49819667?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-2&spm=1001.2101.3001.4242

同一个内存段可以用来存放几种不同类型的成员,但在每一个时刻只能存在其中一种,而不是同时存放几种。也就是说,每一瞬间只有一个成员起作用,其它的成员不起作用,即不是同时都存在和起作用。
共用体变量中起作用的成员是最后一个存放的成员,在存入一个新的成员后,原有的成员就失去作用。

#include <iostream>
using namespace std;
 
union test
{
     char mark;
     long num;
     float score;
}a;
 
int main()
{
     // cout<<a<<endl; // wrong
     a.mark = 'b';
     cout<<a.mark<<endl; // 输出'b'
     cout<<a.num<<endl; // 98 字符'b'的ACSII值
     cout<<a.score<<endl; // 输出错误值
 
     a.num = 10;
     cout<<a.mark<<endl; // 输出换行 非常感谢suxin同学的指正
     cout<<a.num<<endl; // 输出10
     cout<<a.score<<endl; // 输出错误值
 
     a.score = 10.0;
     cout<<a.mark<<endl; // 输出空
     cout<<a.num<<endl; // 输出错误值
     cout<<a.score<<endl; // 输出10
 
     return 0;
}

由于union中的所有成员起始地址都是一样的,所以&a.mark、&a.num和&a.score的值都是一样的。
由于union里面的东西共享内存,所以不能定义静态、引用类型的变量。
在union里也不允许存放带有构造函数、析构函数和复制构造函数等的类的对象,但是可以存放对应的类对象指针。编译器无法保证类的构造函数和析构函数得到正确的调用,由此,就可能出现内存泄漏。

接下来看第二个版本:

class Airplane {
private:
	struct AirplaneRep {
		unsigned long miles;
		char type;
	};
private:
	union {
		AirplaneRep rep;	//此指针指向使用中的对象
		Airplane* next;		//此指针指向free list上的对象
	};
//这些方法不是重点
public:
	unsigned long getMiles() {return rep.miles;}
	char getType() {return rep.type;}
	void set(unsigned long m, char t) {
		rep.miles = m;
		rep.type = t;
	}
//重载new和delete
public:
	static void* operator new(size_t size);
	static void operator delete(void* deadObject, size_t size);
private:
	static const int BLOCK_SIZE;
	static Airplane* headOfFreeList;
};
Airplane* Airplane::headOfFreeList;
const int Airplane::BLOCK_SIZE = 512;

new的重载和第一个版本相似

void* Airplane::operator new(size_t size)
{
	//在继承发生时可能size大小有误
	if(size != sizeof(Airplane))
		return ::operator new(size);
	Airplane* p = headOfFreeList;
	if(p)	//如果p有效就把链表头部向下移
		headOfFreeList = p->next;
	else
	{
		//如果链表已空,申请一大块内存
		Airplane* newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));
		
		//将小块穿成一个freelist
		for(int i = 1; i < BLOCK_SIZE - 1; ++i)
			newBlock[i].next = &newBlock[i+1];
		newBlock[BLOCK_SIZE - 1].next = 0;
		p = newBlock;
		headOfFreeList = &newBlock[1];
	}
	return p;
}

delete版本几乎与第一个版本一样,都存在着没有将内存归还给操作系统的问题,这个并不属于内存泄漏,因为内存仍然掌握在我们手中。

//operator delete截获一个内存块
//如果大小正确,就把它加到freelist前端
void Airplane::operator delete(void* deadObject, size_t size)
{
	if(deadObject == 0) return;
	if(size != sizeof(Airplane)) {
		::operator delete(deadObject);
		return;
	}
	Airplane* carcass = static_cast<Airplane*>(deadObject);
	carcass->next = headOfFreeList;
	headOfFreeList = carcass;
}

最终版本:static allocator

刚刚我们都是针对一个class单独重载它的new和delete,这样会导致代码的重复性。我们将刚刚的动作抽象出来放到一个class里面。
每个allocator object 都是分配器,它体内维护一个freelist,不同的allocator objects维护者不同的freelists。

class allocator
{
private:
	struct obj {
		struct obj* next;
	};
public:
	void* allocate(size_t);
	void deallocate(void*,size_t);
private:
	obj* freeStore = nullptr;
	const int CHUNK = 5;	//这里小一些以便观察
};

void allocator::deallocate(void* p,size_t)
{
	//将*p收回插入free list 前端
	((obj*)p)->next = freeStore;
	freeStore = (obj*)p;
}

void* allocator::allocate(size_t size)
{
	obj* p;
	if(!freeStore) {
		//linked list为空,于是申请一大块
		size_t chunk = CHUNK * size;
		freeStore = p = (obj*)malloc(chunk);
		
		//将分配得来的一大块当作linked list
		//串接起来
		for(int i = 0; i < (CHUNK - 1); ++i) {
			p->next = (obj*)((char*)p + size);
			p = p->next;
		}
		p->next = nullptr;	
	}
	p = freeStore;
	freeStore = freeStore->next;
	return p;
}

下面是是类调用分配器:
每个allocator object 都是分配器,它体内维护一个freelist,不同的allocator objects维护不同的freelists
所有与内存相关的细节由allocator接管。我们的工作是让application classes正确运作。

class Foo {
public:
	long L;
	string str;
	static allocator myAlloc;
public:
	Foo(long l):L(1){}
	static void* operator new(size_t size) {
		return myAlloc.allocate(size);
	}
	static void operator delete(void* pdead,size_t size) {
		return myAlloc.deallocate(pdead,size);
	}
};
allocator Foo::myAlloc;

如上所示具体的内存分配的细节都不再由应用类所知晓。
测试代码如下:注意这里不要使用using namespace std;因为标准库里也有allocator。

#include<cstddef>
#include<iostream>

//using namespace std;
namespace static_allocator
{
	class allocator
	{
	private:
		struct obj {
			struct obj* next;
		};
	public:
		void* allocate(size_t);
		void deallocate(void*, size_t);
	private:
		obj* freeStore = nullptr;
		const int CHUNK = 5;	//这里小一些以便观察
	};

	void allocator::deallocate(void* p, size_t)
	{
		//将*p收回插入free list 前端
		((obj*)p)->next = freeStore;
		freeStore = (obj*)p;
	}

	void* allocator::allocate(size_t size)
	{
		obj* p;
		if (!freeStore) {
			//linked list为空,于是申请一大块
			size_t chunk = CHUNK * size;
			freeStore = p = (obj*)malloc(chunk);

			//将分配得来的一大块当作linked list
			//串接起来
			for (int i = 0; i < (CHUNK - 1); ++i) {
				p->next = (obj*)((char*)p + size);
				p = p->next;
			}
			p->next = nullptr;
		}
		p = freeStore;
		freeStore = freeStore->next;
		return p;
	}

	class Foo {
	public:
		long L;
		std::string str;
		static allocator myAlloc;
	public:
		Foo(long l) :L(1) {}
		static void* operator new(size_t size) {
			return myAlloc.allocate(size);
		}
		static void operator delete(void* pdead, size_t size) {
			return myAlloc.deallocate(pdead, size);
		}
	};
	allocator Foo::myAlloc;
	void test_static_allocator_3()
	{
		std::cout << "\n\n\ntest_static_allocator().......... \n";
		{
			Foo* p[100];

			std::cout << "sizeof(Foo)= " << sizeof(Foo) << std::endl;
			for (int i = 0; i < 23; ++i) {	//23,任意數, 隨意看看結果 
				p[i] = new Foo(i);
				std::cout << p[i] << ' ' << p[i]->L << std::endl;
			}
			//Foo::myAlloc.check();

			for (int i = 0; i < 23; ++i) {
				delete p[i];
			}
			//Foo::myAlloc.check();
		}
	}
}

int main()
{
	static_allocator::test_static_allocator_3();
	return 0;
}

打印效果如下:
针对一个class写出它的内存管理池以及总结出allocator类(三个版本)
可以看到allocator每一次调用malloc都是一次性取5个元素。所以每5个元素一定的是相邻的。可以从效果图的倒数第三个与倒数第四个之间看出。
重点关注这个写法。

上一篇:C++ boost库教程之内存池


下一篇:从源码角度分析string内存分布