在上一篇的内存管理简单实例中,我们用一个 next 指针将分配的一整块内存中的每个对象大小的内存块链接成链表,客户端需要动态分配对象内存时,直接从链表上获取,减少了不必要的 cookie 内存消耗。但是缺点也很明显,就是增加了每个对象的 next 指针的内存消耗。我们可以看看什么时候会使用到 next 指针:
- 当分配了一大块内存,这一大块内存上的每个对象大小的小内存块需要用 next 指针串在一起,注意,此时内存块对于客户端来说处于为分配状态,也就是客户端并没有使用这块内存。
- 当客户端使用 new 申请分配一块内存时,将从事先分配好的内存链表上取出一小块,分配给客户端使用。注意,客户端使用时,next 数组将不再被使用。
- 当客户端使用 delete 将对象内存回收时,这块内存将会被再次插入到链表上。注意,此时又会重新使用 next 指针,串接到链表上。
以上的分析可以看出,当分配内存和回收内存的时候,才会去使用到 next 指针,当该内存被分配给客户端使用时,该 next 指针将不在被使用到。也就是说,对象本身的数据内存和 next 指针的数据内存不会同时被使用。因此,我们可以将对象本身的数据和 next 指针的数据共用同一个内存。于是就使用到了 union 数据类型。 这种类型的指针被称为 embeded pointer
看如下代码:
class Airplane
{
private:
struct AirplaneRep
{
unsigned long miles;
char type;
};
union
{
AirplaneRep rep; //针对使用中的 object,以 AirplaneRep 类型去解释内存数据
Airplane *next; //针对 freelist 上的 object,以指针形式解释内存数据
};
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;
}
void* operator new(size_t size)
{
//如果大小有误
//在发生继承时,可能会产生大小有误的情况
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane* p = headOfFreeList;
if (p)
{
headOfFreeList = headOfFreeList->next;
}
else
{
//申请分配一大块新的内存
Airplane *newBlock = static_cast<Airplane*>(::operator new(BLOCK_SIZE * size));
//将每一小块串成 free list
//跳过第一块小内存,因为它将作为本次的分配内存结果,没有必要串在 list 上
for (int i = 1; i < BLOCK_SIZE; ++i)
{
newBlock[i].next = &newBlock[i + 1];
}
//链表尾部置空
newBlock[BLOCK_SIZE - 1].next = nullptr;
//当前返回地址
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
void operator delete(void* pDead, size_t size)
{
if (pDead == nullptr)
return;
if (size != sizeof(Airplane))
{
::operator delete(pDead);
return;
}
Airplane* p = static_cast<Airplane*>(pDead);
//插入到链表头部
p->next = headOfFreeList;
headOfFreeList = p;
}
private:
static const int BLOCK_SIZE;
//未使用内存块链表头指针
static Airplane* headOfFreeList;
};
当内存被分配使用时,则用 AirplaneRep 及 Airplane 的数据类型去解释;当内存未被分配使用时,则用 next 指针去解释。
这样的话,一个 Airplane 对象只占用 8 个字节内存(内存对齐),next 指针并没有多消耗内存。
Amazing~~