C/C++內存管理(隨筆)

1:內存管理,從廣義上來看,其實可以說是資源管理。

而“資源”包括:內存,IO資源(文件句柄,設備句柄),GDI,(多線程環境下的)各種臨界區資源。

要想實現有效的資源管理,有效的資源回收機制,就必須確保一個前提條件:每個資源都有他們的所有者。即有相應的指針、引用或句柄來指向這些資源。

 

2:頻繁使用new 和 delete 進行不定大小的內存分配,將導致堆上的內存碎片,從而導致程序性能的下降。這是因爲,當存在大量碎片時,一方面這些碎片經常不能滿足new的要求,導致低效的利用率,同時,OS也會花費更多的時間在堆鏈表中尋找可用的內存塊。所以:必須合理的控制內存的分配

但當你必須要使用new 和delete時,你不得不自己控制C++中的內存分配。你需要用一個全局的new 和delete來代替系統的內存分配符,並且一個類一個類的重載new和delete

一個防止堆破碎的通用方法是從不同固定大小的內存池中分配不同類型的對象,對每個類重載new 和delete就提供了這樣的控制,這樣就能提供針對每一個類的相應的內存管理方案。

 

 

3:C++用類對資源管理的準則:對象創建時獲取資源,對象銷燬時釋放資源;對應的也就是:在構造函數中進行資源分配,而在析構函數中釋放所有在構造函數中申請的資源。如果能夠嚴格按照這一準則,就程序中將不會產生任何資源泄漏問題!!

(引文:

一個指針,一個句柄,一個臨界區狀態只有在我們將它們封裝入對象的時候纔會擁有所有者。這就是我們的第一規則:在構造函數中分配資源,在析構函數中釋放資源。

  當你按照規則將所有資源封裝的時候,你可以保證你的程序中沒有任何的資源泄露。這點在當封裝對象(EncapsulatingObject)在棧中建立或者嵌入在其他的對象中的時候非常明顯。但是對那些動態申請的對象呢?不要急!任何動態申請的東西都被看作一種資源,並且要按照上面提到的方法進行封裝。這一對象封裝對象的鏈不得不在某個地方終止。它最終終止在最高級的所有者,自動的或者是靜態的。這些分別是對離開作用域或者程序時釋放資源的保證。

 

4:new實際的操作有兩個步驟組成:1:malloc, 2: constructor, 對應的delete爲: 1:destructor, 2:free.

所以,實際的初始化操作都是在內存分配成功後才執行的。

 

5:如何一個類對象沒有顯式地提供構造函數和析構函數,那麼其行爲特性與基本數據類型的行爲特性在內存構建方面大體上沒有什麼區別。

 

6:基於第4,5點,我們可以分析delete和delete[]的區別:

例:Object* obj = newObject[num];

// deleteobj;//obj佔用的內存空間會被釋放,但只有obj[0]的構造函數會被調用

  delete[]obj;//obj佔用的內存空間會被釋放,並且obj[0]~obj[num-1]所有數組中元素的構造函數都會被調用

所以兩種形式,在Object沒有動態內存分配的情況下(也即沒有指針成員,或只有默認構造函數),是可以認爲等效的。但是,如果有了動態內存分配的情況,則兩者是不同的,決不可混用!

 

7:new[] 和 delete[]

C++將對象數組的內存分配作爲一個單獨的操作,而不同於單個對象的內存分配。即new[] 和 delete[]並不是通過循環來一次次的調用new和delete來實現的。實際上,它會首先一次性完成所需內存大小的分配(實際上是也是調用operator new()來完成內存分配的),然後再一次性的調用數組中每一個元素的構造函數。

 

8:關於重載operator new 和operator delete 以及 operator new[] 和 operator delete[]

由第4點和第7點的分析,我們可以看到,new 和delete的工作都是要分成兩步來完成的,而通過重載operator new/delete, 實際上,是給了我們控制malloc/free的機會,而對於constructor/destructor的調用是C++語言內置的行爲,並不受程序員的控制!

注:實際上,這也很好理解:我們不需要再在operator new/delete中對對象的構造過程進行什麼控制了。這樣做,一來導致分工不明確,二來完全沒必要,因爲,如果想要對構造過程加以控制和修改,你只需要修該相應的類的構造/析構函數就行了,沒必要在operatornew/delete中越俎代庖了。就讓operator new/delete專心去管怎樣有效的使用內存這一件事情吧!

 

9:如何管理類中的成員指針?

解決方案1:只要類中有指針,就必須定義自己版本的拷貝構造函數和賦值構造函數,並且,執行第3條準則。即:使得類的具有值對象的行爲。

解決方案2:明確禁止拷貝和賦值構造函數,使得所有的類的賦值具有指針行爲

解決方案3:智能指針(有兩種實現方式:引用計數、auto_ptr)

解決方案4:內存池

解決方案5:全自動的垃圾回收機制(原理:垃圾回收的時候,只需要掃描 bss 段, data 段以及當前被使用着的棧空間,找到可能是動態內存指針的量,把引用到的內存遞歸掃描就可以得到當前正在使用的所有動態內存了。)

 

10:對於內存的使用優先選擇: 棧 --> 堆--> 

 

11:一個管理內存的疑惑: 內存中動態生成的對象很多,相互調用和引用的關係很複雜,當程序在一直往下寫的時候,實在已經不知道將要被引用的對象是否還存在,這時應該怎麼辦?

答:應該重新設計數據結構,從根本上解決對象管理的混亂局面!

在具體實施措施上,應該在類層次上實現對內存的管理,並且整個項目在內存管理的策略上要保持某種一致性。並參考第9點中的各種策略,結合不同的應用需求,具體問題具體分析。

 

12:可以使用auto_ptr作爲類中的成員變量,從而能夠防止當構造函數失敗時,因爲沒有調用析構函數而造成的資源泄漏問題。

auto_ptr的實質:一種資源封裝類,用棧對象來封裝動態分配的資源,同時禁止auto_ptr自身的動態分配行爲,即不允許auto_ptr在堆上創建實例。

 

13:智能指針的使用,使得資源泄漏的問題限制到局部作用域中,從而對於內存方面的編程錯誤的排查也能夠局部化,更有利於發現和排錯。 

 

14:引用計數:共享的所有權

 

15:將原有代碼轉換爲資源管理代碼

如果你是一個經驗豐富的程序員,你一定會知道找資源的bug是一件浪費時間的痛苦的經歷。我不必說服你和你的團隊花費一點時間來熟悉資源管理是十分值得的。你可以立即開始用這個方法,無論你是在開始一個新項目或者是在一個項目的中期。轉換不必立即全部完成。下面是步驟。

(1)       首先,在你的工程中建立基本的Strong Pointer。然後通過查找代碼中的new來開始封裝裸指針。

(2)       最先封裝的是在過程中定義的臨時指針。簡單的將它們替換爲auto_ptr並且刪除相應的delete。如果一個指針在過程中沒有被刪除而是被返回,用auto_ptr替換並在返回前調用release方法。在你做第二次傳遞的時候,你需要處理對release的調用。注意,即使是在這點,你的代碼也可能更加"精力充沛"--你會移出代碼中潛在的資源泄漏問題。

(3)       下面是指向資源的裸指針。確保它們被獨立的封裝到auto_ptr中,或者在構造函數中分配在析構函數中釋放。如果你有傳遞所有權的行爲的話,需要調用release方法。如果你有容器所有對象,用Strong Pointers重新實現它們。

(4)       接下來,找到所有對release的方法調用並且盡力清除所有,如果一個release調用返回一個指針,將它修改傳值返回一個auto_ptr。

(5)       重複着一過程,直到最後所有new和release的調用都在構造函數或者資源轉換的時候發生。這樣,你在你的代碼中處理了資源泄漏的問題。對其他資源進行相似的操作。

(6)       你會發現資源管理清除了許多錯誤和異常處理帶來的複雜性。不僅僅你的代碼會變得精力充沛,它也會變得簡單並容易維護。

 

 16:如何對付內存泄漏?

很明顯,當你的代碼中到處充滿了new 操作、delete操作和指針運算的話,你將會在某個地方搞暈了頭,導致內存泄漏,指針引用錯誤,以及諸如此類的問題。

這和你如何小心地對待內存分配工作其實完全沒有關係:代碼的複雜性最終總是會超出你能夠付出的時間和努力

於是隨後產生了一些成功的技巧,它們依賴於將內存分配(allocations)與重新分配(deallocation)工作隱藏在易於管理的類型(class)之後。標準容器(standard

 containers)是一個優秀的例子。它們(這些類)不是通過你而是自己爲元素管理內存,從而避免了產生糟糕的結果。

這是因爲:標準容器類內部有一個allocator,可以實現對內存分配的自我管理

 

17:好好利用STL,減少顯式的內存管理的工作

(1)通過使用函數對象標準算法(standard algorithm),我們可以避免使用指針——例如使用迭代子(iterator);

(2)如果你的程序還沒有包含將顯式內存管理減少到最小限度的庫,那麼要讓你程序完成和正確運行的話,最快的途徑也許就是先建立一個這樣的庫

 

18:禁止產生堆對象

你決定禁止產生某種類型的堆對象,這樣,你就可以自己創建一個資源封裝類,該類對象只能在棧中產生,這樣就能在異常的情況下自動釋放封裝的資源。

 

19:如何禁止產生堆對象?

operator new/delete 設爲private

 

20:如何禁止產生棧對象?

將構造/析構函數設爲protected,然後用靜態方法createInstance/destroyInstance來完成對象的創建和銷燬(兩個函數在其內部會調用構造/析構函數)

如果一個類不打算作爲基類,通常採用的方案就是將其析構函數聲明爲private。爲了限制棧對象,卻不限制繼承,我們可以將析構函數聲明爲protected!

  

參考文檔:

C++內存管理

http://www.cnblogs.com/qiubole/articles/1094770.html

 

 

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