在上一篇的內存管理簡單實例中,我們用一個 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~~