c++徹底消滅——內存泄漏、野指針(下篇)

1. 前情回顧

前篇最後,我們爲消除內存泄漏、野指針等問題所做的代碼嘗試還是存在問題,本篇我們來討論一下進一步的改進。

2. 需求總結

根據上一篇,總體上我們還需解決以下問題:

  1. 用戶只new開闢,但是忘記使用delete釋放,程序需要定期自動清理內存。
  2. 在之前的內存管理中,部分指針需要用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 問題總結

上述代碼只是簡單實現,它還存在如下問題:

  1. New和Delete爲新增函數,不符合new和delete的使用習慣;
  2. MemoryManager類中所有指針統一存到vector中,將其按堆內存地址分組存放比較好,指向同一塊堆內存的指針地址放到一起,這樣甚至operator new和operator delete中都不需要引用計數,因爲MemoryManager中相當於進行了引用計數。
  3. 上述代碼還沒有解決忘記Delete或者程序中發生異常,處理異常的代碼中未Delete帶來的內存泄露問題。解決這個需要在2的基礎上,在MemoryManager類增加一個線程函數,定期掃描所有指針是否依然有效,移除無效的指針,並減少引用計數。
  4. 還有數組開闢和釋放的operator new[]以及operator delete[]沒有改造。

總之,上述方案還不是盡善盡美,畢竟,我們爲了保留操作習慣,最終得到的還是原始指針,c++原始指針和"="賦值運算符以及new,delete不具備自動內存管理的功能。本次改進,暫時告一段落,有興趣的同學可以自己繼續改進。

6. 後記

至於上述總結的問題爲什麼我不解決掉,我個人覺得,c++內存管理是一個很複雜的東西,c++本身又以高效著稱。正常情況下,上述方案已經將內存管理的複雜度降低到了我能掌控的程度,再增加一個線程來管理內存,收益不大,反而降低效率,而且,還會帶來未盡的問題。
其實,我覺得,在new和delete中增加對內存標誌字符串HEAP_SIGN_STR都沒有必要,因爲棧指針一般生存期很短,大多是函數局部變量指針,這種情況很容易管理,我們不太可能會忘記其到底是不是堆內存指針,不太可能對其誤用delete。
最後,由於增加了New和Delete函數,其實使用起來不如new和delete習慣,在都不習慣的條件下,其實我們可以使用c++11新增的智能指針來自動管理內存的。也就不需要向上面這樣一頓折騰。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章