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

[摘要]

c++被譽爲最難學的編程語言,一方面是由於其功能過於強大、過於底層,導致語法靈活多變;另一方面是由於其內存管理極其複雜。其中,最主要的,被詬病最多的,就是其內存管理。
c++的內存結構中的使用頻率超高的堆內存完全由程序員自己管理,這就導致c++這門語言對程序員的水平要求極高,一不小心就會導致內存泄漏,或者使用已釋放的內存,進而導致程序輸出與預期不符,甚至導致內存耗盡、程序崩潰等嚴重問題。c++程序界普遍遵守的“誰開闢,誰釋放”也不總是那麼有效,遇到非順序執行程序,比如異常、跳轉等,可能跳過釋放步驟;另外某些共享資源,可能不是“我”開闢的,但是某些條件下,“我”需要釋放。當這些情況遇上多線程,問題將變得更加複雜。
總之,c++程序員需要時刻與內存管理做鬥爭,勞心費力可能最後寫出來的程序還是一堆bug,關鍵是程序員寫程序沒考慮到的地方,問題可能也比較難找。
本文主要目的就是建立一套自動內存管理方案,將c++程序員從繁重的內存管理工作中解放出來,在使用new和delete時再也不用擔心內存釋放和泄露的問題,媽媽再也不用擔心我的內存管理問題啦!!!

1. 需求分析

在c++程序中,new是基本不存在什麼問題的,發生問題一般是關於delete的。下面我們引入兩個人物對話來闡明需求:Ethan大神(哈哈,就是小弟我啦,容許我自戀一下),小A(虛擬人物,不配有名字☺)。

場景1

小A:Ethan大神,你快來看看,我這段程序怎麼崩潰了?
Ethan:讓我看看你的代碼。

小A寫的代碼:

int main()
{
	int a = 5;
	int* pA = &a;
	delete pA;
	pA = nullptr;
	return 0;
}

Ethan:你這個代碼有問題啊,你的變量a是存於棧中,但是卻試圖用delete釋放它,這當然錯了,delete只能釋放堆內存,所以導致崩潰啦。
小A:喔,我知道了,原來問題這麼簡單呀,可是,我現在還沒辦法用好指針,在寫較爲複雜的程序的時候,特別是類內保存的指針或者函數調用參數的指針,我怎麼知道它是開闢在棧內存還是堆內存的呀,難保我不會用delete去釋放它呀,有沒有什麼辦法讓我使用delete的時候自動幫我判斷是否是堆指針,不是堆指針就直接跳過不釋放啊。

好啦,需求1出來啦:

  • 需求1:可以對任意指針使用delete,delete需要自動判斷是否是堆內存。

場景2

小A:Ethan大神,我這又出問題了,麻煩你幫忙看看好不?
Ethan:什麼問題?
小A:你看這段代碼,它又崩潰了。

小A的代碼:

int main()
{
	int* pA = new int;
	int* pA1 = pA;
	delete pA;
	pA = nullptr;
	delete pA1;
	pA1 = nullptr;
	return 0;
}

Ethan:不止代碼被你整崩潰了,我都被你整崩潰了,你這裏只用new申請一次堆內存,但是你卻對這個地址使用了兩次delete。第二次delete釋放一個早已經釋放的地址,當然不允許啦。
小A:原來如此,我是新手嘛,沒法避免這樣的情況呀,對了,Ethan大神,有沒有什麼方法能避免這種情況,讓我不管delete多少次,程序都能智能識別這部分內存是否早已經釋放,這樣我就不用每次使用delete的時候都小心翼翼了。

OK,需求2也出來啦:

  • 需求2:對指向同一對象的多個指針,可以delete多次,delete需要自動判斷內存是否已經得到釋放。
    注意:delete操作符首先是調用對象的析構函數進行析構,再調用operator delete釋放內存,而我們在不創建新的內存管理函數條件下,可以改進的只是operator delete,因此,多次delete會多次調用析構函數,析構函數中有相關操作的需要注意是否已經操作過一次。

2. 解決方案

  1. 針對需求1,網上有方案是通過判斷指針地址值來判斷一個指針指向的地址是堆內存還是棧內存,但是都會存在多種問題;既然c++使用new來申請堆內存,那麼我們是不是可以在申請堆內存的時候做點手腳,表示這是堆內存。自然想到的就是在申請的內存頭部增加一段頭部信息,申明這是堆內存。
  2. 針對需求2,最常見的是採用引用計數,delete一次,引用計數減少一次,直到減少爲0,則真正釋放內存;同時,爲了讓引用計數正確無誤,在指針賦值的時候需要增加引用計數。但是賦值運算符 “=“無法進行全局重載,只能在類內重載,但是類內重載不具有普適性,同時需要對每個類重載”=”,相當麻煩。因此我們需要摒棄使用"="來爲指針賦值,創建新的指針賦值函數或重載其他運算符作爲指針賦值運算符。

3. 初次嘗試

按照上述方案,我們先放一版代碼出來,代碼中有詳細註釋:

#include <memory>
#include <exception>

#define HEAP_SIGN_STR ("HeapYes")	///<堆內存標誌
#define HEAP_FREE_SING_STR ("FreeYes") ///<堆內存釋放標誌
#define NUM_BYTE_HEAP_SIGN 8	///<堆內存標誌大小,爲HEAP_SIGN_STR字符串長度+1

void* operator new(size_t sz)
{
	//分配空間大小=對象大小sz+堆內存標誌大小NUM_BYTE_HEAP_SIGN+引用計數區大小sizeof(int)
	void* p = malloc(sz + NUM_BYTE_HEAP_SIGN + sizeof(int));
	if (!p)
	{
		//分配失敗,拋出異常
		throw std::bad_alloc();
	}
	else
	{
		//將堆內存標誌拷貝到頭部
		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 (strncmp(pStr, HEAP_SIGN_STR, NUM_BYTE_HEAP_SIGN) != 0)
	{
		//如果堆內存標誌不爲HEAP_SIGN_STR,證明指針不是堆指針
		return;
	}
	if (--(*pCount) == 0)
	{
		memcpy(pStr , HEAP_FREE_SING_STR , NUM_BYTE_HEAP_SIGN);
		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;//堆內存標誌區指針
	if (strncmp(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);
}

//指針複製方法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;
	}
private:
	T* m_p;
};

class Ptr :public PtrBase
{
public:
	template<typename T>
	Ptr(T* p)
	{
		m_ptr = (PtrWrapper<T>*)malloc(sizeof(PtrWrapper<T>));
		new(m_ptr) PtrWrapper<T>(p);
	}
	template<typename T>
	void operator &= (T*& pDst)
	{
		try
		{
			pDst = (dynamic_cast<PtrWrapper<T>*>(m_ptr))->Get();
			add_ref(pDst);
		}
		catch (const std::bad_cast& e)
		{
			throw e;
		}
	}
	~Ptr()
	{
		free(m_ptr);
	}
private:
	PtrBase* m_ptr;
};

int main()
{
	int* a = new int;	//只new一次,後面delete了兩次
	int* b = nullptr;
	Ptr(a) &= b;		//重載運算符法複製指針,重載&=表示a並且b相等,指向相同位置。
	//ptr_copy(b, a);	//函數法複製指針,與上面的Ptr(a) &= b二選一
	delete a;			//delete堆內存。
	a = nullptr;
	delete b;			//再次delete相同的堆內存,不會出現問題。
	delete b;			//重複delete已經被釋放的內存,不會出現問題。
	b = nullptr;
	return 0;
}

4. 總結

本篇最後放出的代碼初步解決了本篇開始提出的兩個需求,但是還遠沒有完全解決c++的內存問題,比如,上述代碼必須滿足delete次數比複製次數至少多一次,因爲new的初始引用爲1,這還是需要程序員控制delete使用的次數。
關於上述代碼存在的問題,以及c++內存管理更深入的問題,我抽空寫下一篇文章繼續和大家探討。

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