內存泄露(一)

內存泄露

根據定義,內存泄露是指在堆上分配了一些內存(在C++中,使用new操作符;在C中,使用malloc()或calloc()),然後把這塊內存的地址賦值給一個指針,後來卻丟失了這個值,這可能是由於這個指針因爲離開了作用域而失效。

{
	MyClass* my_class_object = new MyClass;
	DoSomething(my_class_object);
}//內存泄露

或者是因爲向它賦了其他值:

	MyClass* my_class_object = new MyClass;
	DoSomething(my_class_object);
	my_class_object = NULL;//內存泄露

另外還有一種情況,程序員一直分配新內存,並沒有丟失指向它們的指針,但是一直保留着指向程序不再使用的對象的指針。後面這種情況一般並不能算是內存泄露,但它所導致的後果是一樣的:程序將耗盡內存。我們把後面這種錯誤留給程序員來操心,我們的注意力主要是前面的那些情況,即“正式”的內存泄露。

考慮兩個對象,它們包含了指向對方的指針,這種情況稱爲“循環引用”。


指向A和B的指針都存在,但是如果沒有其他任何指針指向這兩個對象,就沒有辦法回收指向任何一個對象的內存,因此導致了內存泄露。這兩個對象將會一直存在,不會被銷燬。現在考慮相反的例子。假設有一個類,它由一個在一個獨立的線程中運行的方法:

class SelfResponsible : public Thread
{
public:
	virtual void Run()
	{
		DoSomethingImportantAndCommitSuicide();
	}

	void DoSomethingImportantAndCommitSuicide()
	{
		sleep(1000);
		delete this;
	}
};

我們在一個獨立的線程中啓動它的Run()方法,如下所示:

Thread* my_object = new SelfResponsible;
my_object->Start(); //在一個獨立的線程中調用Run()方法
my_object = NULL;

我們向這個指針賦了NULL值,丟失了這個對象的地址,從而導致了前面所說的內存泄露。但是,我們深入觀察DoSomethingImportantAndCommitSuicide()方法的內部,將會發現在執行了一些任務之後,這個對象將會刪除自身,把它所佔據的內存釋放給堆,使之可以被複用。因此,它實際上並沒有產生內存泄露。

對於上述這些例子,進一步定義內存泄露。即如果我們分配了內存(使用new操作符),必須由某物(某個對象)負載:

  • 刪除這塊內存
  • 採用正確的方法完成這個任務(使用正確的delete操作符,帶方括號或不帶方括號)
  • 這個任務只執行一次
  • 在完成了對這塊內存的使用之後,應該儘快執行這項任務
這個刪除內存的責任通常稱爲對象的所有權。在前面的例子中,對象具有它自身的所有權。因此我們可以總結如下:內存泄露是由於被分配的內存的所有權丟失了。

以下的示例代碼:

void SomeFunction()
{
	MyClass* my_class_object = NULL;
	//一些代碼.....
	if(SomeCondition1())
	{
		my_class_object = new MyClass;
	}
	//更多代碼
	if(SomeCondition2())
	{
		DoSomething(my_class_object);
		delete my_class_object;
		return;
	}
	//更多代碼
	if(SomeCondition3())
	{
		DoSomethingElse(my_class_object);
		delete my_class_object;
		return;
	}

	delete my_class_object;
	return;
}

我們從NULL指針開始討論的原因是爲了避免“爲什麼只能在堆棧上創建對象,這樣就可以完全避免銷燬對象的問題”這個問題。有許多原因導致不適合在堆棧上創建對象。例如,有時,對象的創建必須延遲到程序中的某個時刻,晚於程序在內存中的變量所創建的時間。或者,它是又其他工廠類創建的,我們所得到的是一個指向它的指針,並需要負責在不需要使用這個對象時將其刪除。另外,我們也可能根本不知道是否將要創建這個對象,就如前面的例子一樣

既然我們已經在堆上創建了一個對象,就要負責刪除它。對於上段的代碼,它存在一些問題。每當我們添加一條額外的return語句時,必須在返回之前刪除這個對象。

但是,即使我們記得在每條return語句之前刪除這個對象,還是沒能解決我們的問題。如果這段代碼所調用的任何函數可能拋出一個異常,實際上意味着我們可能從包含函數調用的任何代碼行“返回”。因此,我們必須把這段代碼放在try-catch語句中,並且當我們捕捉了一個異常時,不要忘了刪除這個對象,然後拋出另一個異常。爲了避免內存泄露,看上去需要做的工作有很多。如果這段代碼中存在負責清理工作的語句,情況就變得更爲複雜了,從而導致很難理解,程序員也很難把注意力集中到實際的工作中。

這個問題的解決方案是使用智能指針,這是C++中許多人使用的辦法。有些模板類的行爲和常規的指針非常相似,但它們擁有所指向的對象的所有權,從而解除了程序員的煩惱。在這種情況下,前面所描述的函數將變成:

void SomeFunction()
{
	SmartPointer<MyClass> my_class_object;
	//一些代碼.....
	if(SomeCondition1())
	{
		my_class_object = new MyClass;
	}
	//更多代碼
	if(SomeCondition2())
	{
		DoSomething(my_class_object);
		return;
	}
	//更多代碼
	if(SomeCondition3())
	{
		DoSomethingElse(my_class_object);
		return;
	}

	return;
}

注意,我們並沒有在任何地方刪除被分配的對象,現在這個責任是由智能指針(my_class_object)所承擔的。

這實際上是一個更爲通用的C++模式的一種特殊情況。在這個模式中,一個對象獲取了一些資源(通常在構造函數中,但並不一定),然後,這個對象就負責釋放這些資源,並且這個任務是在它的析構函數中完成的。使用這個模式的一個例子是在進入一個函數的時候獲取一個Mutex對象的鎖:

void MyClass::MyMethod()
{
	MutexLock lock(&my_mutex_);
	//一些代碼
}  //析構函數~MutexLock()被調用,因此釋放了my_mutex_
在這個例子中,MyClass類具有一個稱爲my_mutex_的數據成員,它必須在一個方法開始的時候獲取,並且在離開這個方法之前被釋放。它是在構造函數中由MutexLock獲取的,並在它的析構函數中被自動釋放,因此我們可以保證不管My::MyMethod()函數內部的代碼發生了什麼(即,不管我們插入多少條return語句或者是否可能拋出異常),這個方法不會在返回之前忘了釋放my_mutex_。

現在,回到內存泄露問題。解決方案是當我們分配心內存時,必須立即把指向這塊內存的指針賦值給某個智能指針。這樣,我們就不需要擔心刪除這塊內存的問題了。這個任務完全由這個智能指針來負責。

此時我們會有以下疑問:

(1)是否允許對智能指針進行復制?

(2)如果是,在智能指針的多份拷貝中,到底由哪一個負責刪除它們共同指向的對象?

(3)智能指針是否表示指向一個對象的指針,或者表示指向一個對象數組的指針(即它應該使用帶方括號的還是不帶方括號的delete操作符)?

(4)智能指針是否對於一個常量指針或一個非常量指針?

對於這些問題的答案,我們可能會面臨許多種不同的智能指針。事實上,在C++的社區討論中,有些人使用了大量的由不同的庫提供的智能指針,例如,較爲突出的是boost庫,但是,多種不用的智能指針容易出現新的錯誤。例如,把一個指向一個對象的指針賦值給一個期望接受一個指向數組的智能指針(即它將使用帶方括號的)就會出現問題。反之亦然。

其中一種智能指針auto_ptr<T>具有一個奇怪的屬性,當我們擁有一個auto指針p1,並像下面這樣創建了它的一份拷貝p2時:

auto_ptr<int> p1(new int);
auto_ptr<int> p2(p1);

指針p1就變成了NULL,這是非常不直觀的,因此很容易產生錯誤。

一般有兩種智能指針可以有效的放置內存泄露:

(1)引用計數指針(又稱共享指針)

(2)作用域指針

這兩種指針的不同之處在於引用計數指針可以被複制,而作用域指針不能被複制。但是,作用域指針的效率更高;

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