程式執行過程中的記憶體活動

鴨脖發表於2012-04-21

預備知識

C++中,記憶體分成5個區,他們分別是堆、棧、自由儲存區、全域性/靜態儲存區和常量儲存區。

              
  

棧,就是那些由編譯器在需要的時候分配,在不需要的時候自動清除的變數的儲存區。裡面的變數通常是區域性變數、函式引數等。

  

 

  
  

堆,就是那些由new分配的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new就要對應一個delete。如果程式設計師沒有釋放掉,那麼在程式結束後,作業系統會自動回收。

  

 

  
  

自由儲存區,就是那些由malloc等分配的記憶體塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。[注] 堆和自由儲存區一般認為是同一名詞。 或者,自由儲存區的一部分被開成堆,剩下的則是未使用。堆和棧都可以在自由儲存區裡 
  進行自動擴充套件,直到2者相接而成為記憶體耗盡。 

  

 

  
  

全域性/靜態儲存區,全域性變數和靜態變數被分配到同一塊記憶體中,在以前的C語言中,全域性變數又分為初始化的和未初始化的,在C++裡面沒有這個區分了,他們共同佔用同一塊記憶體區。

  

 

  
  

常量儲存區,這是一塊比較特殊的儲存區,他們裡面存放的是常量,不允許修改(當然,你要通過非正當手段也可以修改,而且方法很多)[注] 常量儲存區則是“全域性/靜態儲存區”的一部分,在os支援常量段的情況下,編譯器可以把一部分const的全域性變數放在這個位置。而且,這個全域性/靜態儲存區並沒有被CC++標準給出名稱,都是俗稱,各個資料上可以名字差異很大。

  

 

  

 

記憶體中,堆和棧的區別

   主要的區別有以下幾點:

1、管理方式不同;

2、空間大小不同;

3、能否產生碎片不同;

4、生長方向不同;

5、分配方式不同;

6、分配效率不同;

                                   
  

管理方式不同

  
  

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

  
  

空間大小不同

  
  

一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的

  
  

能否產生碎片不同

  

 

  
  

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

  
  

生長方向不同

  
  

對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。

  
  

分配方式不同

  
  

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

  
  

分配效率不同

  
  

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

  

使用棧就像我們去飯館吃飯,直觀點菜(發出申請),付錢和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和稍微工作,他的好處就是快捷,但自由度小。

使用堆就像我們自己動手做喜歡吃的菜餚,比較麻煩,但能做出符合自己口味的菜,自由度大

[]棧物件的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程式設計師操心;而且 棧物件的建立速度一般較堆物件快,因為分配堆物件時,會呼叫operator new操作,operator new會採用某種記憶體空間搜尋演算法,而該搜尋過程可能是很費時間的,產生棧物件則沒有這麼麻煩,它僅僅需要移動棧頂指標就可以了。但是要注意的是,通常棧 空間容量比較小,一般是1MB2MB,所以體積比較大的物件不適合在棧中分配。特別要注意遞迴函式中最好不要使用棧物件,因為隨著遞迴呼叫深度的增加, 所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢位,這樣就會產生執行時錯誤。

堆物件建立和銷燬都要由程式設計師負責,所以,如果處理不好,就會發生記憶體問題。如果分配了 堆物件,卻忘記了釋放,就會產生記憶體洩漏;而如果已釋放了物件,卻沒有將相應的指標置為NULL,該指標就是所謂的“懸掛指標”,再度使用此指標時,就會出現非法訪問,嚴重時就導致程式崩潰。但是高效 的使用堆物件也可以大大的提高程式碼質量。比如,我們需要建立一個大物件,且需要被多個函式所訪問,那麼這個時候建立一個堆物件無疑是良好的選擇,因為我們 通過在各個函式之間傳遞這個堆物件的指標,便可以實現對該物件的共享,相比整個物件的傳遞,大大的降低了物件的拷貝時間。另外,相比於棧空間,堆的容量要 大得多。實際上,當實體記憶體不夠時,如果這時還需要生成新的堆物件,通常不會產生執行時錯誤,而是系統會使用虛擬記憶體來擴充套件實際的實體記憶體。

靜態儲存區。所有的靜態物件、全域性物件都於靜態儲存區分配。關於全域性物件,是在 main()函式執行前就分配好了的。其實,在main()函式中的顯示代 碼執行之前,會呼叫一個由編譯器生成的_main()函式,而_main()函式會進行所有全域性物件的的構造及初始化工作。而在main()函式結束之 前,會呼叫由編譯器生成的exit函式,來釋放所有的全域性物件。除了全域性靜態物件,還有區域性靜態物件通和class的靜態成員,區域性靜態物件是在函式中 定義的,就像棧物件一樣,只不過,其前面多了個static關鍵字。區域性靜態物件的生命期是從其所在函式第一次被呼叫,更確切地說,是當第一次執行到該靜 態物件的宣告程式碼時,產生該靜態區域性物件,直到整個程式結束時,才銷燬該物件。class的靜態成員的生命週期是該class的第一次呼叫到程式的結束。

記憶體分配方式
  記憶體分配方式有三種:
[1]從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static變數。
[2]在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
[3]從堆上分配,亦稱動態記憶體分配。程式在執行的時候用mallocnew申請任意多少的記憶體,程式設計師自己負責在何時用freedelete 放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活,但如果在堆上分配了空間,就有責任回收它,否則執行的程式會出現記憶體洩漏,頻繁地分配和釋放不同大 小的堆空間將會產生堆內碎塊。

[4]  文字常量分配在文字常量區,程式結束後由系統釋放。

應用例項:

這是一個前輩寫的,非常詳細

//main.cpp

int a = 0; 全域性初始化區

char *p1; 全域性未初始化區

main()

{

int b;// 

char s[] = "abc"; //

char *p2; //

char *p3 = "123456"; //"123456\0"在常量區,p3在棧上。

static int c =0 //全域性(靜態)初始化區

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

//分配得來得1020位元組的區域就在堆區。

strcpy(p1, "123456"); //123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。}

static的內部機制:

 靜態資料成員要在程式一開始執行時就必須存在。因為函式在程式執行中被呼叫,所以靜態資料成員不能在任何函式內分配空間和初始化。
       
這樣,它的空間分配有三個可能的地方,一是作為類的外部介面的標頭檔案,那裡有類宣告;二是類定義的內部實現,那裡有類的成員函式定義;三是應用程式的main()函式前的全域性資料宣告和定義處。
      
靜態資料成員要實際地分配空間,故不能在類的宣告中定義(只能宣告資料成員)。類宣告只宣告一個類的尺寸和規格,並不進行實際的記憶體分配,所以在類宣告中寫成定義是錯誤的。它也不能在標頭檔案中類宣告的外部定義,因為那會造成在多個使用該類的原始檔中,對其重複定義。
      static
被引入以告知編譯器,將變數儲存在程式的靜態儲存區而非棧上空間,靜態
資料成員按定義出現的先後順序依次初始化,注意靜態成員巢狀時,要保證所巢狀的成員已經初始化了。消除時的順序是初始化的反順序。

       static的優勢:
       
可以節省記憶體,因為它是所有物件所公有的,因此,對多個物件來說,靜態資料成員只儲存一處,供所有物件共用。靜態資料成員的值對每個物件都是一樣,但它的值是可以更新的。只要對靜態資料成員的值更新一次,保證所有物件存取更新後的相同的值,這樣可以提高時間效率。

        引用靜態資料成員時,採用如下格式:
         <
類名>::<靜態成員名>
    
如果靜態資料成員的訪問許可權允許的話(public的成員),可在程式中,按上述格式

『檔案過大,有興趣者可通過郵箱索要』

相關文章