简单的c++内存池分配

  让我们一起来用c++写一个简单的内存池分配器吧!
  首先我们要知道为什么需要内存分配器:

  1. 内存碎片
    什么是内存碎片呢?

int* a = new int();
int* b = new int();
int* c = new int();
int* d = new int();
int* e = new int();
delete b;
delete d;
double* f = new double();
  举例上面的代码来看一段内存
在这里插入图片描述
一块空的连续内存

在这里插入图片描述
依次申请5个int型(5x4字节)变量

在这里插入图片描述
释放其中b和d
  上面的操作产生了两块内存碎片,当我们想申请double f的时候发现没有位置可以存下8个字节的f变量了!
  我们的pc每时每刻都在运行着许多软件,这些软件每时每刻不断申请内存和释放操作,内存碎片不断积累,最终会造成难以想象的内存浪费。我们可以通过设计内存分配器来限制某程序或者程序局部变量的内存申请范围,以此来减少内存碎片。内存池分配器就是其中一种。

  1. 优化内存分配
      我们可以设计一个良好的内存分配器来提高程序的存储效率、减少内存开销、方便程序设计等。比如含对齐功能的分配器可以自动帮我们类型对齐,不需要在写代码时去手动注意内存对齐(在堆上申请的内存编译器不会帮我们做内存补齐的);游戏引擎的单帧和双缓存分配器,可以自动在每帧(一个循环)结束释放内存块等。

  好了,废话不多说了,开始我们的内存池分配器设计吧!
  一步一步来,请看第一版本代码:

#include "pch.h"
#include <iostream>
class Malloc_int_16
{
 int* head;
public:
 Malloc_int_16()
 {
  head = (int*)malloc(64);
 }
 int* GetPtr_int()
 {
  return head;
 }
 ~Malloc_int_16()
 {
  free(head);
  std::cout << "free now";
 }
};
int main()                //测试一下
{
 Malloc_int_16 mallocA;
 int* a = mallocA.GetPtr_int();
 *a = 99;
 std::cout << *a;
 system("PAUSE ");
}

  上面几行代码完成了我们的第一个版本的4x4的int型内存池分配器,对象mallocA的构造预先申请了一块4x4x4字节的连续内存,然后我们可以在编程的时候像使用new命令字一样使用方法mallocA.GetPtr_int()来申请一个int型变量,并获得它的地址。上面我们不需要去管int变量的释放,即使它是在堆上申请的,因为当对象mallocA被析构的时候会自动释放整个内存块。
  显然这个内存分配器是鸡肋的,一是它申请了64个字节的内存,却只能使用4个字节,二是它不能手动“释放”变量a,当我们不需要a的时候,这个a占据的内存就会浪费。
  再来看第二个版本:

#include "pch.h"
#include <iostream>
class Malloc_int_16
{
 int* head = NULL;
 int count = 0;
public:
 Malloc_int_16()
 {
  head = (int*)malloc(sizeof(int)*16);
 }
 int* GetPtr_int()
 {
  if (count < 16)
  {
   count++;
   return (head + count - 1);
  }
  else
   return NULL;
 }
  void FreePtr_int()
 {
  if (count > 0)
  {
   count--;
  }
 }
 ~Malloc_int_16()
 {
  free(head);
  std::cout << "free now";
 }
};
int main()                //测试一下
{
 Malloc_int_16 mallocA;
 int* a = mallocA.GetPtr_int();
 int* b = mallocA.GetPtr_int();
 *a = 99;
 *b = 100;
 std::cout << *a << *b;
 mallocA.FreePtr_int();
 int* c = mallocA.GetPtr_int();
 *c = 101;
 std::cout << *c;
 system("PAUSE ");
}

  上面的代码把在堆上的内存分配器做成了一个像在栈上的内存分配器,每次申请一个int型变量,都会从内存块中顺序分出4个字节,并返回访问指针,每调用一次mallocA.FreePtr_int();会逆序“释放”(实际上只把分配索引减一,并没有对原来数据做操作,这点可以自行加入其它处理)。
  显然第二个版本的设计能给将我们预申请的64个字节的内存块充分使用,但是缺陷是它不能指定释放变量,而是要逆序释放,这是我们无法容忍的缺陷。
  接下来的你可能会想一个设计,能够充分申请内存块的内存,又能够随心所欲地释放我们不想要的变量。
  最简单的思路是对象mallocA有一个int *ptrArr[16]数组成员,在构造函数中申请内存块之后,把64个字节的内存块分成16个4字节的小块,使用循环把16个小块内存的地址保存到数组ptrArr里面。count索引初始化为最大,每调用一次GetPtr_int(),就把数组ptrArr最后一个元素返回,然后索引count减1。每调用一次FreePtr_int(int *p),就把索引count加1,把p的地址保存到数组ptrArr[count]元素。虽然多次申请释放后,数组保存的地址不再顺序对应内存块的地址,但是却可以灵活申请和释放。从功能上看这个方案确实很好,但是我们想一下,数组ptrArr和索引count浪费了我们64+4个字节的内存,而我们可用的内存块才64个字节啊!这无疑是自找麻烦的分配器了!
  我们可以这样想,想要灵活的申请释放内存,必须要有一个容器保存所有小内存块的地址,但是这个容器又要内存开销,怎么办呢?其实我们可以利用申请的64个字节的内存块本身来做这个容器!
  第三版本的代码如下:

#include "pch.h"
#include <iostream>
class Malloc_int_16
{
 int* head = NULL;
 int* listHead = NULL;//指向可分配元素
public:
  Malloc_int_16()
 {
  listHead = head = (int*)malloc(sizeof(int)*16);
  
  for (int i = 0; i < 16; i++)
  {
   *(head + i) = (int)(head + i + 1);//第一个元素保存下一个元素的地址,注意把地址强制转换成int型
  }
  *(head + 15) = 0;//设置最后一个可分配元素保存的是0
 }
  int* GetPtr_int()
 {
  if (listHead == NULL)       //内存已经分配完了
  {
   std::cout << "NULL"<< std::endl;
   return NULL;
  }
  if (*listHead == 0)       //这是最后一个可分配元素
  {
   int* tmpPtr = listHead;
   listHead = NULL;
   return tmpPtr;
  }
  int* tmpPtr = listHead;
  listHead = (int*)(*listHead);   //设置下一个可分配元素
  return tmpPtr;
 }
  void FreePtr_int(int* p)
 {
  if (listHead == NULL)
  {
   listHead = p;
   *p = 0;       //增加了第一个可分配元素
   return;
  }
  *p = (int)listHead;
  listHead = p;
 }
 ~Malloc_int_16()
 {
  free(head);
  std::cout << "free now";
 }
};
int main()                //测试一下
{
 Malloc_int_16 mallocA;
 int* a = mallocA.GetPtr_int();
 std::cout << "a的地址是 " << a << std::endl;
 *a = 99;
 std::cout << *a<< std::endl;
 int* b = mallocA.GetPtr_int();
 std::cout << "b的地址是 "<< b << std::endl;
 //mallocA.FreePtr_int(a);
 int* c = mallocA.GetPtr_int();
 std::cout << "c的地址是 " << c << std::endl;
 int* d = mallocA.GetPtr_int();
 std::cout << "d的地址是 " << d << std::endl;
 mallocA.FreePtr_int(a);
 mallocA.FreePtr_int(b);
 int* e = mallocA.GetPtr_int();
 std::cout << "e的地址是 " << e << std::endl;
 int* f = mallocA.GetPtr_int();
 std::cout << "f的地址是 " << f << std::endl;
 int* g = mallocA.GetPtr_int();
 std::cout << "g的地址是 " << g << std::endl;
 system("PAUSE ");
}

  以上是第三版本的代码,使用预分配的内存块做成一个链表,链表节点没有data,只有next。大家可以把代码中分配的16个int型改成3个,然后看main函数的输出。
  虽然已经很取巧了,但是我们还是难免在mallocA对象里多存储一个head指针和一个listHead指针,花销了8个字节,我实在想不到有什么方法可以避免了,除非你要在预申请的内存块中牺牲一块内存来保存额外的数据。
  看到这里你可能想问:”如果我的申请的内存单元不是int型该怎么办?“比如我想做的分配器分配单元是2字节的,或者更小,可是我们一个地址至少要4个字节才能存储,也就是说分配单元本身不能存储地址那么大的数。
  怎么办呢?其实可以想一下,地址太大了,能不能换一种小数据来记录链表节点之间的联系?能!我们可以不存储地址,而是存储索引!我们观察到额外开销里面有一个head指针,这个是确定而且不变的,我们可以通过head + 索引的方式访问到所有的分配单元。比如上面代码的16分配器,索引是0到15,最大数是15,只要分配单元足够存下15这个数就行了。假设你设定的分配单元
是n位的,那么你的内存池的总分配单元数不超过2^n个就行了。

  第三版本的分配器会有内存块耗尽的风险,如果返回一个NULL而不加以检查的话会出问题,如果检查到返回NULL就要再构建一个新的Malloc_int_16类型的对象,获得一个新的预分配内存块。我们做一个更智能的类,能够检查到内存块耗尽,自动构建新对象。思路很简单,另写一个新类,包含Malloc_int_16对象成员指针,构造函数里初始化一个Malloc_int_16对象,当检查到Malloc_int_16对象返回内存指针为NULL时,自动新构造一个Malloc_int_16对象,并使用新对象分配内存。当然,当“前一个”对象的分配单元被释放的时候可能要整理内存,销毁多余对象,有些麻烦。
  此外,上面代码可以进行一些优化和扩展,比如使用模板来写一个泛型类,这样就不限定于int型分配单元了,本文不再赘述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值