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

C++內存管理學習筆記(1)

編輯:C++入門知識

/****************************************************************/

/*            學習是合作和分享式的!

/* Author:Atlas                   
/*  轉載請注明本文出處:
 

/****************************************************************/

一 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: 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字節的區域就在堆區。  13:     strcpy(p1, "123456"); //123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。  14: }
例子3:


   1: char* f1()     2: {     3: char* p = NULL;     4: char a;     5: p = &a;     6: return p;     7: }     8: char* f2()     9: {    10: char* p = NULL:    11: p =(char*) new char[4];    12: return p;    13: }
       這兩個函數都是將某個存儲空間的地址返回,二者有何區別呢?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可是相當困難。

 

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