1. 前情回顧
前篇最後,我們爲消除內存泄漏、野指針等問題所做的代碼嘗試還是存在問題,本篇我們來討論一下進一步的改進。
2. 需求總結
根據上一篇,總體上我們還需解決以下問題:
- 用戶只new開闢,但是忘記使用delete釋放,程序需要定期自動清理內存。
- 在之前的內存管理中,部分指針需要用delete,部分指針不能用delete,這導致使用者的困惑。後續改進中,需要支持對所有指針使用delete,程序自動判斷被delete的指針之前是否觸發過引用計數,若沒觸發,則不減少引用計數。
3. 問題分析
目前,現有的內存管理方案都是如下圖所示:
多個指針指向同一個內存,他們都知道自己指向誰,但是堆內存卻不知道誰指向自己,因此,在自己被釋放的時候無法進行控制。
針對上述需求1,我們需要讓堆內存知道誰指向自己,定期查詢其是否還指向自己,如果不指向自己,則減少引用。
針對上述需求2,我們在拷貝指針的時候需要向堆內存註冊該指針,讓堆內存記住該指針,在內存釋放的時候經過註冊的指針纔有資格釋放。這樣可以避免內存被沒有權限的指針釋放。
要做的事情已經清晰了,但是,這裏還有一個問題,operator new和operator delete的各種形式本身並不具備反向地址傳遞,也就是說,operator new和operator delete天生無法知道到底是哪個指針指向對內存。因此,若要解決上述問題,內存開闢和釋放就不能使用new和delete了。
4. 解決方案
我們先看代碼:
#include <memory>
#include <exception>
#include <mutex>
#include <vector>
#define HEAP_SIGN_STR ("HeapYes") ///<堆內存標誌
#define NUM_BYTE_HEAP_SIGN 8 ///<堆內存標誌大小,爲HEAP_SIGN_STR字符串長度+1
static std::mutex memUseLock;
class MemoryManager
{
MemoryManager(const MemoryManager&) = delete;
MemoryManager& operator = (const MemoryManager&) = delete;
MemoryManager() {}
public:
static MemoryManager& GetInstance()
{
static MemoryManager instance;
return instance;
}
//提交指針至內存管理系統
template<typename T>
void commit(T*& p)
{
for (auto iter_ptr = m_ptr.begin(); iter_ptr != m_ptr.end(); iter_ptr++)
{
if ((void*)(&p) == (*iter_ptr))
{
return; //已經存在,無法重複提交同一個指針
}
}
m_ptr.push_back((void*)(&p));
}
//從內存管理系統移除指針
template<typename T>
void remove(T*& p)
{
for (auto iter_ptr = m_ptr.begin(); iter_ptr != m_ptr.end();)
{
if ((void*)(&p) == (*iter_ptr))
{
iter_ptr = m_ptr.erase(iter_ptr);
delete p;
return;
}
else
{
iter_ptr++;
}
}
return;
}
private:
std::vector<void*> m_ptr;
};
template<typename T, typename ...Args>
void New(T*& p, Args&&... args)
{
p = new T(std::forward<Args>(args)...);
MemoryManager::GetInstance().commit(p);
}
template<typename T>
void Delete(T*& p)
{
MemoryManager::GetInstance().remove(p);
p = nullptr;
}
void* operator new(size_t sz)
{
//分配空間大小=對象大小sz+堆內存標誌大小NUM_BYTE_HEAP_SIGN+引用計數區大小sizeof(int)
void* p;
while ((p = malloc(sz + NUM_BYTE_HEAP_SIGN + sizeof(int))) == 0)
{
if (_callnewh(sz + NUM_BYTE_HEAP_SIGN + sizeof(int)) == 0)
{
//分配失敗,拋出異常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
//將堆內存標誌拷貝到頭部
memcpy(p, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN);
p = (char*)p + NUM_BYTE_HEAP_SIGN;
//初始化引用計數爲1
*((int*)p) = 1;
//將指針指向對象數據區,並返回該指針,後續對象在該數據區構造存儲
p = (char*)p + sizeof(int);
return p;
}
void operator delete(void* p)
{
int* pCount = (int*)((char*)p - sizeof(int)); //引用計數區指針
char* pStr = (char*)pCount - NUM_BYTE_HEAP_SIGN;//堆內存標誌區指針
if (memcmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
{
//如果堆內存標誌不爲HEAP_SIGN_STR,證明指針不是堆指針
return;
}
std::lock_guard<std::mutex> lock(memUseLock); //加鎖防止重複釋放內存
if (--(*pCount) == 0)
{
free((void*)pStr);
return;
}
//!=0時,要麼是已經釋放過了;要麼是還有引用,不應該釋放;
//兩種情況都應該直接返回。
return;
}
//增加引用計數,指針複製時自動調用,對用戶透明
inline void add_ref(void* p)
{
int* pCount = (int*)((char*)p - sizeof(int)); //引用計數區指針
char* pStr = (char*)pCount - NUM_BYTE_HEAP_SIGN;//堆內存標誌區指針
std::lock_guard<std::mutex> lock(memUseLock);
if (memcmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
{
//如果堆內存標誌不爲HEAP_SIGN_STR,證明指針不是堆指針
return;
}
++(*pCount);
}
//指針複製方法1:函數法。
template<typename Tx, typename Ty>
inline void ptr_copy(Tx*& pDst, Ty* pSrc)
{
pDst = pSrc;
add_ref((void*)pSrc);
MemoryManager::GetInstance().commit(pDst);
}
//指針複製方法2:重載操作符
class PtrBase
{
public:
virtual ~PtrBase() {}
PtrBase() {}
PtrBase(const PtrBase&) = delete;
PtrBase& operator = (const PtrBase&) = delete;
};
template<typename T>
class PtrWrapper :public PtrBase
{
public:
PtrWrapper(T* p)
{
m_p = p;
}
T* Get()
{
return m_p;
}
~PtrWrapper()
{
delete m_p;
}
private:
T* m_p;
};
class Ptr :public PtrBase
{
public:
template<typename T>
Ptr(T*& p)
{
m_ptr = new PtrWrapper<T>(p);
add_ref(p);
}
template<typename T>
Ptr(T*&& p)
{
m_ptr = new PtrWrapper<T>(p);
}
template<typename T>
void operator &= (T*& pDst)
{
try
{
pDst = (dynamic_cast<PtrWrapper<T>*>(m_ptr))->Get();
add_ref(pDst);
MemoryManager::GetInstance().commit(pDst);
}
catch (const std::bad_cast & e)
{
throw e;
}
}
~Ptr()
{
delete m_ptr;
}
private:
PtrBase* m_ptr;
};
int main()
{
int* z = nullptr;
int* y = nullptr;
New(z); //帶參數時可用New(z, 1);
Ptr(z) &= y;
Delete(z);
Delete(y);
}
5. 代碼分析
上述代碼在之前的基礎上簡單實現了一個內存管理的單例類,配合新的內存分配和釋放函數New和Delete使用,New開闢內存時,將指針和內存進行雙向綁定,Delete時,將指針和內存解綁定。Ptr類也會在指針拷貝時,將新指針和內存雙向綁定。
5.1 作用
這樣,用戶可以對任意指針使用Delete,如果Delete的堆指針是函數參數指針或者是用戶通過 “=”賦值的指針,這些指針未提交到MemoryManager中,因此,不會真正釋放內存。用戶需要關注的只是,一旦你的代碼中有指針,在超出作用域的時候,記得Delete它就可以了。
注意:使用時,需要遵循:使用New函數代替new表達式開闢內存,使用Delete函數代替delete表達式。
5.2 問題總結
上述代碼只是簡單實現,它還存在如下問題:
- New和Delete爲新增函數,不符合new和delete的使用習慣;
- MemoryManager類中所有指針統一存到vector中,將其按堆內存地址分組存放比較好,指向同一塊堆內存的指針地址放到一起,這樣甚至operator new和operator delete中都不需要引用計數,因爲MemoryManager中相當於進行了引用計數。
- 上述代碼還沒有解決忘記Delete或者程序中發生異常,處理異常的代碼中未Delete帶來的內存泄露問題。解決這個需要在2的基礎上,在MemoryManager類增加一個線程函數,定期掃描所有指針是否依然有效,移除無效的指針,並減少引用計數。
- 還有數組開闢和釋放的operator new[]以及operator delete[]沒有改造。
總之,上述方案還不是盡善盡美,畢竟,我們爲了保留操作習慣,最終得到的還是原始指針,c++原始指針和"="賦值運算符以及new,delete不具備自動內存管理的功能。本次改進,暫時告一段落,有興趣的同學可以自己繼續改進。
6. 後記
至於上述總結的問題爲什麼我不解決掉,我個人覺得,c++內存管理是一個很複雜的東西,c++本身又以高效著稱。正常情況下,上述方案已經將內存管理的複雜度降低到了我能掌控的程度,再增加一個線程來管理內存,收益不大,反而降低效率,而且,還會帶來未盡的問題。
其實,我覺得,在new和delete中增加對內存標誌字符串HEAP_SIGN_STR都沒有必要,因爲棧指針一般生存期很短,大多是函數局部變量指針,這種情況很容易管理,我們不太可能會忘記其到底是不是堆內存指針,不太可能對其誤用delete。
最後,由於增加了New和Delete函數,其實使用起來不如new和delete習慣,在都不習慣的條件下,其實我們可以使用c++11新增的智能指針來自動管理內存的。也就不需要向上面這樣一頓折騰。