1. 程式人生 > >C++記憶體管理學習堆和棧

C++記憶體管理學習堆和棧

C++記憶體管理

1.記憶體分配方式 

  在講解記憶體分配之前,首先,要了解程式在記憶體中都有什麼區域,然後再詳細分析各種分配方式。

1.1 C語言和C++記憶體分配區

  下面的三張圖,圖1圖2是一種比較詳細的C語言的記憶體區域分法。圖3是典型的C++記憶體分佈圖,簡單易懂;以下記憶體分配圖,區別就是圖1和2則分為初始化和未初始化靜態變數區,圖3中是全域性變數區。

  C語言(圖1和圖2):(由低地址到高地址)

  a)正文段:用來存放程式執行程式碼。通常,正文段是可共享的。另外,正文段常常是隻讀的,一次防止程式由於意外修改其自身。

  b)初始化資料段:用來存放程式中已初始化的全域性變數。資料段屬於靜態記憶體分配。

  c)非初始化資料段:通常稱為BSS段, 用來存放程式中未初始化的全域性變數。BSS是英文Block Started by Symbol(由符號開始的塊)的簡稱。BSS段屬於靜態記憶體分配。 在程式開始執行之前,核心將此段中的資料初始化為0或者空指標

  d):堆是用於存放程序執行中被動態分配的記憶體段,它的大小並不固定,可動態擴張或縮減。當程序呼叫malloc/free等函式分配記憶體時,新分配的記憶體就被動態新增到堆上 (堆被擴張)/釋放的記憶體從堆中被剔除(堆被縮減)。

  e):棧又稱堆疊, 存放程式的區域性變數(但不包括static宣告的變數,static意味著在資料段中存放變數)。除此以外,在函式被呼叫時,棧用來傳遞引數和返回值

。(為執行函式而分配的區域性變數、函式引數、返回地址等存放在棧區)。由於棧 的先進先出特點,所以棧特別方便用來儲存/恢復呼叫現場。

\
\

  圖1 典型C語言記憶體分佈區域 (UNIX高階環境程式設計) 圖2 典型C語言記憶體分佈區域

  C++(圖3):

  根據《C++記憶體管理技術內幕》一書,在C++中,記憶體分成5個區,他們分別是堆,棧,自由存續區,全域性/靜態存續區,常量存續區

  a) :記憶體由編譯器在需要時自動分配和釋放。通常用來儲存區域性變數和函式引數。(為執行函式而分配的區域性變數、函式引數、返回地址等存放在棧區)。棧運算分配內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。

  b) :記憶體使用new進行分配,使用delete或delete[]釋放。如果未能對記憶體進行正確的釋放,會造成記憶體洩漏。但在程式結束時,會由作業系統自動回收。

  c) 自由儲存:使用malloc進行分配,使用free進行回收。和堆類似。

  d) 全域性/靜態儲存區:全域性變數和靜態變數被分配到同一塊記憶體中,C語言中區分初始化和未初始化的,C++中不再區分了。(全域性變數、靜態資料、常量存放在全域性資料區

  e) 常量儲存區:儲存常量,不允許被修改。

  這裡,在一些資料中是這樣定義C++記憶體分配的,可程式設計記憶體在基本上分為這樣的幾大部分:靜態儲存區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。

  a)靜態儲存區:記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。它主要存放靜態資料、全域性資料和常量

  b)棧區:在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。

  c)堆區:亦稱動態記憶體分配。程式在執行的時候用malloc或new申請任意大小的記憶體,程式設計師自己負責在適當的時候用free或 delete釋放記憶體。動態記憶體的生存期可以由我們決定,如果我們不釋放記憶體,程式將在最後才釋放掉動態記憶體。 但是,良好的程式設計習慣是:如果某動態記憶體不再使用,需要將其釋放掉,否則,我們認為發生了記憶體洩漏現象。

\

  圖3 典型c++記憶體區域

  總結:C++與C語言的記憶體分配存在一些不同,但是整體上就一致的,不會影響程式分析。就C++而言,不管是5部分還是3大部分,只是分法不一致,將5部分中的c)d)e)合在一起則是3部分的a)。

1.2 區分堆、棧、靜態儲存區

  我們通過程式碼段來看看對這樣的三部分記憶體需要怎樣的操作和不同,以及應該注意怎樣的地方。

(1)靜態儲存區與棧區

  1: char* p = “Hello World1”; 2: char a[] = “Hello World2”; 3: p[2] = ‘A’; 4: a[2] = ‘A’; 5: char* p1 = “Hello World1;”

  這個程式是有錯誤的,錯誤發生在p[2] = ‘A’這行程式碼處,為什麼呢,是變數p和變數陣列a都存在於棧區的(任何臨時變數都是處於棧區的,包括在main()函式中定義的變數)。但是,資料 “Hello World1”和資料“Hello World2”是儲存於不同的區域的。

  因為資料“Hello World2”存在於陣列中,所以,此資料儲存於棧區對它修改是沒有任何問題的。因為指標變數p僅僅能夠儲存某個儲存空間的地址,資料“Hello World1”為字串常量,所以儲存在靜態儲存區。雖然通過p[2]可以訪問到靜態儲存區中的第三個資料單元,即字元‘l’所在的儲存的單元。但是因為 資料“Hello World1”為字串常量,不可以改變,所以在程式執行時,會報告記憶體錯誤。並且,如果此時對p和p1輸出的時候會發現p和p1裡面儲存的地址是完全相 同的。換句話說,在資料區只保留一份相同的資料

(2)堆與棧區別

  我們先通過例子1來直觀的說明下棧與堆記憶體的區別,然後在細緻分析例子2中的情況。

  例子1:

  1: void fn(){ 2: int* p = new int[5]; 3: }

  看到new,首先應該想到,我們分配了一塊堆記憶體,那麼指標p呢? 它分配的是一塊棧記憶體,所以這句話的意思就是:棧記憶體中存放了一個指向一塊堆記憶體的指標p程式會先確定在堆中分配記憶體的大小,然後呼叫 operator new分配記憶體,然後返回這塊記憶體的首地址,放入棧中

  注意:這裡為了簡單並沒有釋放記憶體,那麼該怎麼去釋放呢? 是deletep麼? NO,錯了,應該是delete [ ] p,這是告訴編譯器:刪除的是一個數組

  例子2:

 1 int a = 0; //全域性初始化區
 2 char *p1; //全域性未初始化區
 3 int main()
 4 {
 5    int b; //
 6    char s[] = "abc"; //
 7    char *p2; //
 8    char *p3 = "123456"; // 123456\0在常量區,p3在棧上。 
 9    static int c =0; //全域性(靜態)初始化區 
10    p1 = (char *)malloc(10); 
11    p2 = (char *)malloc(20); 12: //分配得來得10和20位元組的區域就在堆區。 
12    strcpy(p1, "123456"); //123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。 
13  }

  例子3:

 1 char* f1() 
 2 { 
 3     char* p = NULL; 
 4     char a; 
 5     p = &a;
 6     return p; 
 7 } 
 8 
 9 
10 char* f2() 
11 { 
12     char* p = NULL;
13     p =(char*) new char[4]; 
14     return p; 
15 }

  這兩個函式都是將某個儲存空間的地址返回,二者有何區別呢?f1()函式雖然返回的是一個儲存空間,但是此空間為臨時空間。也就是說,此空間只 有短暫的生命週期,它的生命週期在函式f1()呼叫結束時,也就失去了它的生命價值,即:此空間被釋放掉。所以,當呼叫f1()函式時,如果程式中有下面 的語句:

  1: char* p ; 2: p = f1(); 3: *p = ‘a’;

  此時,編譯並不會報告錯誤,但是在程式執行時,會發生異常錯誤。因為,你對不應該操作的記憶體(即,已經釋放掉的儲存空間)進行了操作。但是,相 比之下,f2()函式不會有任何問題。因為,new這個命令是在堆中申請儲存空間,一旦申請成功,除非你將其delete或者程式終結,這塊記憶體將一直存 在。也可以這樣理解,堆記憶體是共享單元,能夠被多個函式共同訪問。如果你需要有多個數據返回卻苦無辦法,堆記憶體將是一個很好的選擇。但是一定要避免下面的 事情發生:

  1: void f() 2: { 3: … 4: char * p; 5: p = (char*)new char[100]; 6: … 7: }

  這個程式做了一件很無意義並且會帶來很大危害的事情。因為,雖然申請了堆記憶體,p儲存了堆記憶體的首地址。但是,此變數是臨時變數,當函式呼叫結 束時p變數消失。也就是說,再也沒有變數儲存這塊堆記憶體的首地址,我們將永遠無法再使用那塊堆記憶體了。但是,這塊堆記憶體卻一直標識被你所使用(因為沒有到 程式結束,你也沒有將其delete,所以這塊堆記憶體一直被標識擁有者是當前您的程式),進而其他程序或程式無法使用。我們將這種不道德的“流氓行為” (我們不用,卻也不讓別人使用)稱為記憶體洩漏(memory leak)

  綜合以上兩個例子,我們可以總結一下堆與棧到底有哪些區別!

  (1)管理方式不同

  對於來講,是由編譯器自動管理,無需我們手工控制;對於來說,釋放工作由程式設計師控制,容易產生memory leak。

  (2)空間大小不同

  空間大小:一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於來講,一般都是有一定的空間大小的,例如,在VC6.0下面預設的棧空間大小是1M,可以修改這個值。

  (3)能否產生碎片不同

  對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題, 因為棧是先進後出的佇列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出。

  (4)生長方向不同

  對於來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。(詳見第一部分的記憶體分配圖)

  (5)分配方式不同

  都是動態分配的,沒有靜態分配的堆。有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloca函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

  (6)分配效率不同

  是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比 較高。則是C/C++函式庫提供的,它的機制是很複雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/作業系統)在堆 記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分 到足夠大小的記憶體,然後進行返回。顯然,堆的效率比棧要低得多

  總結:

  堆和棧相比,由於大量new/delete的使用,容易造成大量的記憶體碎片;由於沒有專門的系統支援,效率很低;由於可能引發使用者態和核心態的 切換,記憶體的申請,代價變得更加昂貴。所以棧在程式中是應用最廣泛的,就算是函式的呼叫也利用棧去完成,函式呼叫過程中的引數,返回地址,EBP和區域性變 量都採用棧的方式存放。所以,推薦大家儘量用棧,而不是用堆。

  雖然棧有如此眾多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的記憶體空間,還是用堆好一些。

  無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要麼是程式崩潰,要麼是摧毀程式的堆、棧結構,產生以想不到 的結果,就算是在你的程式執行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難。