程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> [C/C++學習]之十五、內存管理

[C/C++學習]之十五、內存管理

編輯:C++入門知識


索引:
1、內存分配簡介

2、內存分配常見錯誤

3、new()/delete()函數的使用

4、malloc()/free()函數的使用

 

在C++中,內存分成5個區,他們分別是堆、棧、自由存儲區、全局/靜態存儲區和常量存儲區。
棧,就是那些由編譯器在需要的時候分配,在不需要的時候自動清楚的變量的存儲區。裡面的變量通常是局部變量、函數參數等。
堆,就是那些由new分配的內存塊,他們的釋放編譯器不去管,由我們的應用程序去控制,一般一個new就要對應一個delete。如果程序員沒有釋放掉,那麼在程序結束後,操作系統會自動回收。
自由存儲區,就是那些由malloc等分配的內存塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。
全局/靜態存儲區,全局變量和靜態變量被分配到同一塊內存中,在以前的C語言中,全局變量又分為初始化的和未初始化的,在C++裡面沒有這個區分了,他們共同占用同一塊內存區。
常量存儲區,這是一塊比較特殊的存儲區,他們裡面存放的是常量,不允許修改
明確區分堆與棧
在bbs上,堆與棧的區分問題,似乎是一個永恆的話題,由此可見,初學者對此往往是混淆不清的,所以我決定拿他第一個開刀。
首先,我們舉一個例子:
void f() { int* p=new int[5]; }
這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆內存,那麼指針p呢?他分配的是一塊棧內存,所以這句話的意思就是:在棧內存中存放了一個指向一塊堆內存的指針p。在程序會先確定在堆中分配內存的大小,然後調用operator new分配內存,然後返回這塊內存的首地址,放入棧中,他在VC6下的匯編代碼如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
這裡,我們為了簡單並沒有釋放內存,那麼該怎麼去釋放呢?是delete p麼?澳,錯了,應該是delete []p,這是為了告訴編譯器:我刪除的是一個數組,VC6就會根據相應的Cookie信息去進行釋放內存的工作。
好了,我們回到我們的主題:堆和棧究竟有什麼區別?
主要的區別由以下幾點:
1、管理方式不同;
2、空間大小不同;
3、能否產生碎片不同;
4、生長方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程序員控制,容易產生memory leak。
空間大小:一般來講在32位系統下,堆內存可以達到4G的空間,從這個角度來看堆內存幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認的棧空間大小是1M(好像是,記不清楚了)。當然,我們可以修改:
打開工程,依次操作菜單如下:Project->Setting->Link,在Category 中選中Output,然後在Reserve中設定堆棧的最大值和commit。
注意:reserve最小值為4Byte;commit是保留在虛擬內存的頁文件裡面,它設置的較大會使棧開辟較大的值,可能增加內存的開銷和啟動時間。
碎片問題:對於堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對於棧來講,則不會存在這個問題,因為棧是先進後出的隊列,他們是如此的一一對應,以至於永遠都不可能有一個內存塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出,詳細的可以參考數據結構,這裡我們就不再一一討論了。
生長方向:對於堆來講,生長方向是向上的,也就是向著內存地址增加的方向;對於棧來講,它的生長方向是向下的,是向著內存地址減小的方向增長。
分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很復雜的,例如為了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然後進行返回。顯然,堆的效率比棧要低得多。
從這裡我們可以看到,堆和棧相比,由於大量new/delete的使用,容易造成大量的內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用堆。
雖然棧有如此眾多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的內存空間,還是用堆好一些。
無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要麼是程序崩潰,要麼是摧毀程序的堆、棧結構,產生以想不到的結果,就算是在你的程序運行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的:)


在C/C++中,有以下幾種內存分配方式:
1. 從靜態存儲區分配:此時的內存在程序編譯的時候已經分配好,並且在程序的整個運行期間都存在。全局變量,static變量等在此存儲。
全局區(靜態區)( static )存放全局變量、靜態數據、常量。程序結束後有系統釋放
文字常量區 常量字符串就是放在這裡的。 程序結束後由系統釋放。
2. 在棧區分配:相關代碼執行時創建,執行結束時被自動釋放。局部變量在此存儲。棧內存分配運算內置於處理器的指令集中,效率高,但容量有限。
3. 在堆區分配:動態分配內存。用new/malloc時開辟,delete/free時釋放。生存期由用戶指定,靈活。但有內存洩露等問題
棧區( stack )    由編譯器自動分配釋放 ,存放為運行函數而分配的局部變量、函數參數、返回數據、返回地址等。其操作方式類似於數據結構中的棧。
 堆區( heap )     一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由 OS 回收 。分配方式類似於鏈表。

動態分配時可能產生內存碎片,最終動態分配內存會浪費不必要的時間開銷。
動態分配失敗,需要檢查返回值或捕獲異常,這樣也會造成額外的開銷。
動態創建的對象可能被刪除多次或者刪除後繼續使用,這樣就會發生運行時錯誤或程序消耗內存。
[cpp] 
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、內存洩露
在堆上對內存進行申請的時候,一定要提高警覺程度。 動態內存的申請與釋放必須配對,程序中malloc 與free 、new與delete的一定要成對。千萬防止光申請不釋放的代碼出現。
如果發生這種錯誤,函數每被調用一次就丟失一塊內存。
我們來看一下如何防止:
我們將指針放進對象的內部並且讓對象管理指針是最簡單的防止內存洩露的方法。應把new返回的指針存儲在對象的成員變量裡,當需要時該對象的析構函數便能夠釋放掉該指針。
這種方法的優點就是將指針完全交給對象去管理,不需要擔心在操作指針時出現內存洩露等問題。   但是指針僅有初步創建和釋放的語義,通過把指針放進對象裡可以保證該析構函數的執行及正確釋放分配的內存。
C/C++中,內存管理器不會自動回收不再使用的內存,如果忘記釋放不再使用的內存,這些內存就允許被重用,此時就會造成內存洩露。

2、內存越界
何謂內存訪問越界,簡單的說,你向系統申請了一塊內存,在使用這塊內存的時候,超出了你申請的范圍。
讀越界:讀了不屬於自己的數據。    如果讀的內存地址是無效的就會崩潰。
寫越界:也叫做 緩沖區溢出,  在寫書的數據對別人來說是隨機的,這樣也會發生錯誤。
解決:遇到這種問題,首先你得找到哪裡有內存訪問越界,而一個比較麻煩得問題在於,出現錯誤得地方往往不是真正內存越界得地方。對於內存訪問越界,往往需要進行仔細得代碼走查、單步跟蹤並觀察變量以及在調試環境得幫助下對變量進行寫入跟蹤
[cpp]
char b[16]="abcd"; 
memset(b, 0,32);//越界 

3、野指針
使用free或delete釋放了內存後,沒有將指針設置為NULL,也就是指向不可用內存區域的指針。
野指針”不是NULL指針,是指向“垃圾”內存的指針。人們一般不會錯用NULL指針,因為用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。野指針的成因主要有三種:
1、指針變量沒有被初始化。任何指針變量剛被創建時不會自動成為NULL指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要麼將指針設置為NULL,要麼讓它指向合法的內存。
[cpp] 
char *p =NULL;    
char *str = (char *)malloc(100); 
char *p =NULL; char *str = (char *)malloc(100); 

2、指針p被free或者delete之後,沒有置為NULL,讓人誤以為p是個合法的指針。
3、指針操作超越了變量的作用范圍。這種情況讓人防不勝防。
[cpp]
class A    
{    
    public void Func(void)    
     {    
         cout << "Func of class A" << endl;    
     }    
};      
       
void Test(void)      
{      
     A *p;    
     {    
         A a;      
         p = &a;         //    注意    a    的生命期      
     }    
     p->Func();             //    p是“野指針”   

在Test執行語句p->Func()時,對象a已經消失,而p是指向a的,所以p就成了野指針。


4、分配不成功,使用該內存
編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為NULL。如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行。
如果使用malloc()或new()來申請內存,應該使用if(q == NULL)或if(q != NULL)進行防錯處理。

5、分配成功,但未初始化
犯這種錯誤主要有兩個起因:
一是沒有初始化的觀念;
二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組)。
內存的缺省初值究竟是什麼並沒有統一的標准,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。

6、試圖修改常量
在函數參數前加上const修飾符,只是給編譯器做類型檢查用的。編譯器進制修改這樣的變量,但是並不強制,我們完全可以使用強制類型轉換來處理,一般不會出錯。
然後,我們的全局常量和字符串使用強制類型轉換來處理在運行時仍然會出錯。因為他們是放在“rodata”裡面的,而“rodata”內存頁面時不允許修改的。

7、返回指向臨時變量的指針
棧裡面的變量是臨時的,當前函數執行完成時,相關的臨時變量和參數都被清除了。不允許把只想這些臨時變量的指針返回給調用者,因為這樣的指針指向的數據是隨機的,會給程序帶來嚴重後果。

8、對同一指針刪除兩次
[cpp] 
A* a = new A(); 
A* b = a; 
delete a; 
delete b; 
上面的代碼就出現了兩次刪除同一個指針的問題了。
我們第一次delete會安全的釋放掉*a,並且由a所指向的內存會被安全地返回到堆上。   而我們並沒有返回該指針new的操作就把相同的指針第二次傳遞到delete()函數中,而且把之前*a中的對象傳遞給析構函數,然後把由a指向的內存第二次傳遞給該堆,這樣的操作是災難性的,因為這可能會破壞該堆及其自由內存表。
當我們不再使用指針並試圖把該指針刪除時,一定要慎重地考慮如何刪除,而且一個指針只能刪除一次。

9、p指向數組,調用delete p
當指針p指向數組時,不允許執行delete p操作。這是因為當p指向某種內置類型的數組時不能省略delete p[]中的[]。
在delete p中,底層解除分配原語是operator delete (void*),而delete [] p底層解除分配原語是operator delete[] (void*)。

10、內存耗盡
解決方法:
1、判斷指針是否為NULL, 如果為NULL, 則使用exit(1)函數終止整個程序的運行。
2、判斷指針是否為NULL, 如果為NULL, 則使用return 語句終止本函數。
3、為new和malloc()函數預設異常處理函數。
4、捕獲new拋出的異常信息,並試圖恢復。


new()/delete()函數的使用:
來看一下new的使用:
[cpp] 
<span style="font-size:18px;">int *p = new int [length];</span> 
他相對於下面的malloc簡單多了,這是因為new內置了sizeof,類型轉換和類型安全檢查功能,對於非內部數據類型而言,new在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那麼new的語句也可以有多種形式:
[cpp] 
<span style="font-size:18px;">class A 

public: 
A(void); 
A(int x); 
... 
}; 
 
void T(void) 

A *a = new A; 
A *b = new A(1); 
... 
delete a; 
delete b; 
}</span> 
但是當我們用new創建對象數組的時候,只能使用對象的無參數構造函數:
A *a = new A[100];
我們不能創建的同時賦初值:
A *a = new A[100](1);
創建了當然就要銷毀了:
delete [] a;

當我們使用delete去delete this時,要注意哪些問題?
1、必須使用new分配的this對象,而不是用new[]。
2、包含delete this的成員函數必須是在this對象上最後調用的成員函數。
3、在delete this後,該成員函數的剩余部分一定不要接觸this對象的任何部分,包括調用任何其他成員函數或者接觸任何數據成員。
4、在delete this後沒有其他部分代碼並且不能檢查this指針本身。
5、確保沒有其他人對該對象進行刪除。
delete this還會用在線程的消亡上,當線程由於某種原因不能正常結束的時候,會選擇該方式結束,釋放一些資源。

malloc()/free()函數的使用:
先來看一下malloc:
[cpp] 
<span style="font-size:18px;">void* malloc(size_t size); 
int *p = (int*)malloc(sizeof(int) * length);</span> 
我們用malloc申請了一塊長度為length的整形類型的內存,  因為malloc()返回的類型是void*  所以在調用函數的時候要進行類型轉換。
malloc函數本身並不識別要申請的內存是什麼類型,只關心內存的總字節數。這裡我們就可以用sizeof。

free函數原型:
[cpp] 
<span style="font-size:18px;">void free(void* memblock);</span> 
可以看出,free沒有malloc復雜,因為指針p的類型以及它所指向的內存的容量事先都知道,語句free(p)能正確地釋放內存。如果p是NULL指針,那麼free()函數對p無論操作多少次都不會出問題,如果不是NULL指針,那麼free函數對p連續操作兩次就會導致程序運行錯誤。

 

malloc與free是C/C++語言的標准庫函數,new與delete是C++的運算符,它們都可以用於申請動態內存和釋放內存。
對於非內存數據類型的對象而言,光是用malloc/free無法滿足動態對象的要求,對象在創建的同時要自動執行構造函數,兌現公仔消亡前也要自動執行析構函數。由於malloc和free是庫函數不是運算符,不在編譯器控制權之內,不嫩鞏固把執行構造函數和析構函數的任務強加於malloc和free;
C++需要一個能完成動態內存分配和初始化工作的運算符new,以及能完成清理與釋放內存工作的運算符delete。
在這裡,如果我們用free釋放new創建的動態對象,那麼對象因無法執行析構函數而可能導致程序出錯。如果用delete釋放malloc申請的動態內存,理論上可行,但是可讀性差。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved