C++ 內存管理中內存泄漏問題產生原因以及解決方法


C++內存管理中內存泄露(memory leak)一般指的是程序在申請內存后,無法釋放已經申請的內存空間,內存泄露的積累往往會導致內存溢出。

一、內存分配方式

通常內存分配方式有以下三種:

(1)從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。

(2)在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。

(3)從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete釋放內存。動態內存的生存期由程序員決定,使用非常靈活,但如果在堆上分配了空間,就有責任回收它,否則運行的程序會出現內存泄漏,頻繁地分配和釋放不同大小的堆空間將會產生堆內碎塊。

二、程序內存空間

一個程序將操作系統分配給其運行的內存分為五個區域:

(1)棧區:由編譯器自動分配釋放,存放為函數運行的局部變量,函數參數,返回數據,返回地址等。操作方式與數據結構中的類似,棧區有以下特點:

  1)由系統自動分配。比如在函數運行中聲明一個局部變量int b = 10;,系統自動在棧中為b開辟空間;

  2)只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。

(2)堆區:一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收;分配方式類似於鏈表,堆區有以下特點:

  1)需要程序員自己申請,並指明大小,在C中是有malloc函數,在C++中多使用new運算符(從C++角度上說,使用new分配堆空間可以調用類的構造函數,而malloc()函數僅僅是一個函數調用,它不會調用構造函數,它所接受的參數是一個unsigned long類型。同樣,delete在釋放堆空間之前會調用析構函數,而free函數則不會)。

  2)在操作系統中有一個記錄空閑內存地址的表,這是一種鏈式結構。它記錄了有哪些還未使用的內存空間。當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序。

(3)全局數據區:也叫做靜態區,存放全局變量,靜態數據。程序結束后由系統釋放。

(4)文字常量區:可以理解為常量區,常量字符串存放這里。程序結束后由系統釋放。“常量”是指它的值是不可變的,同時,雖然常量也是存儲在內存的某個地方,但是無法訪問常量的地址的

(5)程序代碼區:存放函數體的二進制代碼。但是代碼段中也分為代碼段和數據段。

一個程序內存分配例子:

int a = 0; //全局初始化區
char *p1;  //全局未初始化區
int main() {
    int b; //棧區
    char s[] = /"abc/"; //棧區
    char *p2; //棧區
    char *p3 = /"123456/"; //123456//0在常量區,p3在棧區。
    static int c =0;//全局(靜態)初始化區
    p1 = new char[10];
    p2 = new char[20];
    //分配得來得和字節的區域就在堆區。
    strcpy(p1, /"123456/"); //123456//0放在常量區,編譯器可能會將它與p3所指向的/"123456/"優化成一個地方。
}

三、內存溢出原因

(1)在類的構造函數和析構函數中沒有匹配的調用new和delete函數

  兩種情況下會出現這種內存泄露:

  1)在堆里創建了對象占用了內存,但是沒有顯示地釋放對象占用的內存;

  2)在類的構造函數中動態的分配了內存,但是在析構函數中沒有釋放內存或者沒有正確的釋放內存。

(2)沒有正確地清除嵌套的對象指針

(3)在釋放對象數組時在delete中沒有使用方括號

  方括號是告訴編譯器這個指針指向的是一個對象數組,同時也告訴編譯器正確的對象地址值並調用對象的析構函數,如果沒有方括號,那么這個指針就被默認為只指向一個對象,對象數組中的其他對象的析構函數就不會被調用,結果造成了內存泄露。如果在方括號中間放了一個比對象數組大小還大的數字,那么編譯器就會調用無效對象(內存溢出)的析構函數,會造成堆的奔潰。如果方括號中間的數字值比對象數組的大小小的話,編譯器就不能調用足夠多個析構函數,結果會造成內存泄露。

  釋放單個對象、單個基本數據類型的變量或者是基本數據類型的數組不需要大小參數,釋放定義了析構函數的對象數組才需要大小參數。

(4)指向對象的指針數組不等同於對象數組

  對象數組是指:數組中存放的是對象,只需要delete [ ] p,即可調用對象數組中的每個對象的析構函數釋放空間

  指向對象的指針數組是指:數組中存放的是指向對象的指針,不僅要釋放每個對象的空間,還要釋放每個指針的空間,delete [ ] p只是釋放了每個指針,但是並沒有釋放對象的空間,正確的做法,是通過一個循環,將每個對象釋放了,然后再把指針釋放了。

(5)缺少拷貝構造函數

  兩次釋放相同的內存是一種錯誤的做法,同時可能會造成堆的奔潰。

  按值傳遞會調用(拷貝)構造函數,引用傳遞不會調用。

  在C++中,如果沒有定義拷貝構造函數,那么編譯器就會調用默認的拷貝構造函數,會逐個成員拷貝的方式來復制數據成員,如果是以逐個成員拷貝的方式來復制指針被定義為將一個變量的地址賦給另一個變量。這種隱式的指針復制結果就是兩個對象擁有指向同一個動態分配的內存空間的指針。當釋放第一個對象的時候,它的析構函數就會釋放與該對象有關的動態分配的內存空間。而釋放第二個對象的時候,它的析構函數會釋放相同的內存,這樣是錯誤的。

  所以,如果一個類里面有指針成員變量,要么必須顯示的寫拷貝構造函數和重載賦值運算符,要么禁用拷貝構造函數和重載賦值運算符。

(6)缺少重載賦值運算符

  這種問題跟上述問題類似,也是逐個成員拷貝的方式復制對象,如果這個類的大小是可變的,那么結果就是造成內存泄露.

(7)關於nonmodifying運算符重載的常見錯誤

  1)返回棧上對象的引用或者指針(也即返回局部對象的引用或者指針)。導致最后返回的是一個空引用或者空指針,因此變成野指針(指向被釋放的或者訪問受限內存的指針);

  2)返回內部靜態對象的引用;

  3)返回一個泄露內存的動態分配的對象。導致內存泄露,並且無法回收。

解決這一類問題的辦法是重載運算符函數的返回值不是類型的引用,二應該是類型的返回值,即不是 int&而是int。

(8)沒有將基類的析構函數定義為虛函數

  當基類指針指向子類對象時,如果基類的析構函數不是虛函數,那么子類的析構函數將不會被調用,子類的資源沒有正確是釋放,因此造成內存泄露。

造成野指針的原因:

  1)指針變量沒有被初始化(如果值不定,可以初始化為NULL);

  2)指針被free或者delete后,沒有置為NULL, free和delete只是把指針所指向的內存給釋放掉,並沒有把指針本身干掉,此時指針指向的是“垃圾”內存。釋放后的指針應該被置為NULL;

  3)指針操作超越了變量的作用范圍,比如返回指向棧內存的指針就是野指針;

  4)shared_ptr循環引用。

(9)析構的時候使用void*

  delete掉一個void*類型的指針,導致沒有調用到對象的析構函數,析構的所有清理工作都沒有去執行從而導致內存的泄露。

(10)構造的時候淺拷貝,釋放的時候調用了兩側delete

四、常見解決辦法

(1)shared_ptr共享的智能指針:

  shared_ptr使用引用計數,每一個shared_ptr的拷貝都指向相同的內存。在最后一個shared_ptr析構的時候,內存才會被釋放。

注意事項: 

  1)不要用一個原始指針初始化多個shared_ptr;

  2)不要再函數實參中創建shared_ptr,在調用函數之前先定義以及初始化它;

  3)不要將this指針作為shared_ptr返回出來;

  4)要避免循環引用。

(2)unique_ptr獨占的智能指針:

  1)unique_ptr是一個獨占的智能指針,他不允許其他的智能指針共享其內部的指針,不允許通過賦值將一個unique_ptr賦值給另外一個 unique_ptr;

  2)unique_ptr不允許復制,但可以通過函數返回給其他的unique_ptr,還可以通過std::move來轉移到其他的unique_ptr,這樣它本身就不再 擁有原來指針的所有權了;

  3)如果希望只有一個智能指針管理資源或管理數組就用unique_ptr,如果希望多個智能指針管理同一個資源就用shared_ptr。

(3)weak_ptr弱引用的智能指針:

  弱引用的智能指針weak_ptr是用來監視shared_ptr的,不會使引用計數加一,它不管理shared_ptr內部的指針,主要是為了監視shared_ptr的生命 周期,更像是shared_ptr的一個助手。 weak_ptr沒有重載運算符*和->,因為它不共享指針,不能操作資源,主要是為了通過shared_ptr獲得資源的監測權,它的構造不會增加引用計數,它的析構不會減少引用計數,純粹只是作為一個旁觀者來監視shared_ptr中關連的資源是否存在。 weak_ptr還可以用來返回this指針和解決循環引用的問題。

(4)set_new_handler(out_of_memroy);   //注意參數傳遞的是函數的地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM