为什么要自定义class的内存分配

通过自定义内存分配,可以实现一次性申请一大块内存作为内存池给到 class 使用(减少 malloc 的调用),另一方面也可以通过减少额外创建的 Cookie 从而达到减少内存开销的作用。

使用链表来管理内存池

#include <cstddef>
#include <iostream>

using namespace std;

class Screen {
  public:
    // Screen(int x) : i(x) {};
    Screen(int x) : i(x), next(NULL) {};
    int get() {return i;}

    void* operator new(size_t);
    void operator delete(void*, size_t);
  private:
    int i;
    Screen* next;  // 这里会额外多出一个指针
    static Screen *freeStore;
    static const int screenChunk;
};

Screen* Screen::freeStore = 0;
const int Screen::screenChunk = 24;

void* Screen::operator new(size_t size) {
  Screen *p;
  if (!freeStore) {
    // 申请链表
    size_t chunk = screenChunk * size;
    freeStore = p = reinterpret_cast<Screen*>(new char[chunk]);
    // 将这一大块内存分割为各部分,作为链表使用
    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) {
  // 将 deleted object 插到 freeStore 的下一个,然后 freeStore 往后退一格
  (static_cast<Screen*>(p))->next = freeStore;
  freeStore = static_cast<Screen*>(p);
}

int main() {
  cout << sizeof(Screen) << endl;

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

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

  // 输出地址,比较其间隔
  for (uint i = 0; i < N; ++i) cout << p[i] << endl;

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

<aside> 💡 需要注意的一点是,由于编译器的实现不同,实际元素的间隔不一定如上面显示的一样标准。而且因为系统内存不足,导致申请到的内存不是连续的,显示间隔不为8,这也是有可能的。

</aside>

使用 embedded pointer 来构建链表

通过 embedded pointer, 可以借用已经分配内存,但实际还没有数据的内存的前4个字节作为指针用于连接链表。

代码如下所示:

#include <cstddef>
#include <iostream>

using namespace std;

class Airplane {
  private:
    struct AirplaneRep {
      unsigned long miles;  // 4 bytes
      char type;
    };

    union {
      AirplaneRep rep;
      Airplane *next;  // 在使用next访问时,对 AirplaneRep 的前4个字节,当成指针看待
    };

  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;
    }

    static void* operator new(size_t);
    static void operator delete(void*, size_t);

  private:
    static const int BLOCK_SIZE;
    static Airplane *headOfFreeList;
};

Airplane* Airplane::headOfFreeList = 0;
const int Airplane::BLOCK_SIZE = 512;

void* Airplane::operator new(size_t size) {
  // 如果大小有误,就转交给 ::operator new()
  if (size != sizeof(Airplane)) return ::operator new(size);

  Airplane *p = headOfFreeList;
  if (p)  // 如果p有效,就将list头部下移一个元素
    headOfFreeList = p->next;
  else {
    // freeList 已空,需要申请内存
    Airplane *newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * sizeof(Airplane)));

    // 将小块串成链表, 但跳过第一个元素,因为它将作为本次返回的结果
    for (int i = 1; i < BLOCK_SIZE - 1; ++i) {
      // 这里就说明了,在每个 AirplaneRep 中,当其没有元素的时候,
      // 就使用其前面的4个字节作为指针,指向链表的下一个元素
      // 而当赋值之后,自然该元素就从链表中移除
      newBlock[i].next = &newBlock[i+1];
    }
    newBlock[BLOCK_SIZE-1].next = 0;  // 结束list
    p = newBlock;
    headOfFreeList = &newBlock[1];
  }
  return p;
}

// operator delete 参数是一块内存的指针
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;
}

int main() {
  cout << sizeof(Airplane) << endl;

  size_t const N = 10;
  Airplane *p[N];

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

  // 添加元素
  p[1]->set(1000, 'A');
  p[5]->set(2000, 'B');
  p[9]->set(20000, 'C');

  // 输出地址,比较其间隔
  for (unsigned int i = 0; i < N; ++i) cout << p[i] << endl;

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

这样的链表结果如右图所示

也可以有这种写法。当对象实际获得这块内存时,拿到的指针是一个 char* ,指向某个 union obj。此时虽然对象没有记录这块内存长度的信息,但由于这块内存是给这个对象使用的,故 object ctor 自然不会越界。具体可以参见

static allocator

class myAllocator {
  private:
    struct obj {
      struct obj* next;  // 指向下一个元素的指针
    };
  public:
    void* allocate(size_t);
    void deallocate(void*, size_t);

  private:
    obj* freeStore = nullptr;
    const int CHUNK = 5;
};

相比于第一种方式,这种方式要更加节约内存,设计的也更为巧妙。