c++中堆、棧內存分配


首先來看看主要有幾種程序內存分配:

一個由C/C++編譯程序占用內存分為以下幾個部分
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數參數值,局部變量值等。其操作方式類似於數據結構中棧。
2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中堆是兩回事,分配方式倒是類似於鏈表,呵呵。
3、全局區(靜態區)(static)—,全局變量和靜態變量存儲是放在一塊,初始化全局變量和靜態變量在一塊區域, 未初始化全局變量和未初始化靜態變量在相鄰另一塊區域。 - 程序結束后由系統釋放。-->分別是data區,bbs區 
4、文字常量區 —常量字符串就是放在這里。 程序結束后由系統釋放-->coment區
5、程序代碼—存放函數體二進制代碼。-->code(text)區

經典實例:

#include <string>
int a=0;    //全局初始化區
char *p1;   //全局未初始化區
void 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);   //分配得來的10和20字節的區域就在堆區。
    strcpy(p1,"123456");   //123456\0放在常量區,編譯器可能會將它與p3所向"123456\0"優化成一個地方。
}

 

 

一、堆和棧究竟有什么區別(堆和棧理論知識)?

2.1申請方式
stack:
  由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間
heap:
  需要程序員自己申請,並指明大小,在c中malloc函數
    如p1 = (char *)malloc(10);
    如p2 = (char *)malloc(10);

  在C++中用new運算符:

    int *p = new int(10);   // new一個整型數

  但是注意p、p1、p2本身是在棧中。

 

2.2申請后系統響應
棧:只要棧剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出
堆:首先應該知道操作系統有一個記錄空閑內存地址鏈表,當系統收到程序申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點空間分配給程序,另外,對於大多數系統,會在這塊內存空間中首地址處記錄本次分配大小,這樣,代碼中delete語句才能正確釋放本內存空間。另外,由於找到堆結點大小不一定正好等於申請大小,系統會自動將多余那部分重新放入空閑鏈表中。

 

2.3申請大小限制

棧:在Windows下,棧是向低地址擴展數據結構(棧向下生長),是一塊連續內存區域。這句話意思是棧頂地址和棧最大容量是系統預先規定好,在WINDOWS下,棧大小是2M(也有說是1M,總之是一個編譯時就確定常數),如果申請空間超過棧剩余空間時,將提示overflow。因此,能從棧獲得空間較小。

堆:堆是向高地址擴展數據結構(堆向上生長),是不連續內存區域。這是由於系統是用鏈表來存儲空閑內存地址,自然是不連續,而鏈表遍歷方向是由低地址向高地址。堆大小受限於計算機系統中有效虛擬內存。由此可見,堆獲得空間比較靈活,也比較大。

 

2.4申請效率比較
棧效率高:棧由系統自動分配,速度較快。但程序員是無法控制。
堆效率相對比較低:堆是由new分配內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.

另外,在WINDOWS下,最好方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。

 

2.5堆和棧中存儲內容
棧: 在函數調用時,第一個進棧是主函數中后下一條指令(函數調用語句下一條可執行語句)地址,然后是函數各個參數,在大多數C編譯器中,參數是由右往左入棧,然后是函數中局部變量。注意靜態變量是不入棧
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存地址,也就是主函數中下一條指令,程序由該點繼續運行。

堆:一般是在堆頭部用一個字節存放堆大小。堆中具體內容有程序員安排。

 

2.6存取效率比較

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在運行時刻賦值;

而bbbbbbbbbbb是在編譯時就確定;
但是,在以后存取中,在棧上數組比指針所指向字符串(例如堆)快。
比如:

#include
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}

對應匯編代碼

10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al

第一種在讀取時直接就把字符串中元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,在根據edx讀取字符,顯然慢了。

2.7小結:
堆和棧區別可以用如下比喻來看出:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等准備工作和洗碗、刷鍋等掃尾工作,他好處是快捷,但自由度小。
使用堆就象是自己動手做喜歡吃菜餚,比較麻煩,但是比較符合自己口味,而且自由度大。

 

全局變量或者靜態變量,它們都放在堆里
局部變量放在棧里
堆區,也叫自由存儲區.
為什么說在堆上分配內存比在棧上分配內存慢

  1.堆空間開辟需要用系統函數,棧上直接修改指針.
  2. 堆空間管理需要系統記帳,棧上空間可以由編譯器管理或是保存在某個處理器寄存器中。
  3. 堆空間釋放需要系統管理,棧上釋放可以直接丟棄。堆空間需要通過棧上指針間接引用,所以訪問會慢;


記得在apue2上面看到關於線程中有這樣一段話,大致意思是,一個線程有自己堆棧,可以在堆棧上分配內存,比如說一個結構體,如果這個線程調用了pthread_exit()返回這個結構體指針時候之后要特別小心,因為很有可能這個結構體里面成員值發生改變,這個可以理解,因為同一個進程所有線程資源是共享,當這個線程退出之后那部分以前用過堆棧很可能被其它線程占用,但同時又說如果malloc就不會出現這樣問題,
比如,在棧上分一個int,只要esp-4就可以了,
在堆上系統要記錄被分配內存信息,以便釋放
BTW:
棧有序
堆無序
----------------------------------
內存分配方式有三種:   
   
   1.從靜態存儲區域分配。內存在程序編譯時候就已經分配好,這塊內存在程序整個運行期間都存在。例如全局變量,static變量。   
   
   2.在棧上創建。在執行函數時,函數內局部變量存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器指令集中,效率很高,但是分配內存容量有限。   
   
   3.從堆上分配,亦稱動態內存分配。程序在運行時候用malloc或new申請任意多少內存,程序員自己負責在何時用free或delete釋放內存。動態內存生存期由我們決定,使用非常靈活,但問題也最多。
----------------------------------------
一般所說堆棧(stack)往往是指棧,先進后出,它是一塊內存區。用以存放程序局部變量,臨時變量,函數參數,返回地址等。在這塊區域中變量分配和釋放由系統自動進行。不需要用戶參與。   

   而在堆(heap,先進先出)上空間則是由用戶進行分配,並由用戶負責釋放。

二、 函數調用與堆棧

1)編譯器一般使用棧來存放函數的參數,局部變量等來實現函數調用。有時候函數有嵌套調用,這個時候棧中會有多個函數的信息,每個函數占用一個連續的區域。一個函數占用的區域被稱作幀()。同時棧是線程獨立的,每個線程都有自己的棧。例如下面簡單的函數調用:

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

另外函數堆棧的清理方式決定了當函數調用結束時由調用函數或被調用函數來清理函數幀,在VC中對函數棧的清理方式由兩種:

  參數傳遞順序 誰負責清理參數占用的堆棧
__stdcall 從右到左 被調函數
__cdecl 從右到左 調用者

 

2) 有了上面的知識為鋪墊,我們下面細看一個函數的調用時堆棧的變化:

代碼如下:

 

int Add(int x, int y)
{
    return x + y;
}
void main()
{
    int *pi = new int(10);
    int *pj = new int(20);
    int result = 0;
    result = Add(*pi,*pj);
    delete pi;
    delete pj;
}

 

對上面的代碼,我們分為四步,當然我們只畫出了我們的代碼對堆棧的影響,其他的我們假設它們不存在,哈哈!

第一,int *pi = new int(10);   int *pj = new int(20);   int result = 0; 堆棧變化如下:

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

第二,Add(*pi,*pj);堆棧如下(函數參數入棧:從右向左):

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

第三,將Add的結果給result,堆棧如下:

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

第四,delete pi;    delete pj; 堆棧如下:

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

第五,當main()退出后,堆棧如下,等同於main執行前,哈哈!

堆棧與函數調用(高手) - 毛狼 - maolang0 的博客

 

 

 例子1 ----------------------------------------------------------------------------

【參見】http://blog.csdn.net/wudaijun/article/details/8135205
#include<iostream> using namespace std; int main() { char p[] ="123456"; // char s[10]; // 正常復制: 123456 -- 123456 char s[4]; // 棧溢出(目標棧空間不夠大), output: 56 -- 123456 char *ptr = p + 3; strcpy(s, p); cout<< p << " -- " << s << " --- " << ptr << endl; return 0; } // 棧 內存分配方式 (地址:高(左)->低(右); 數據寫入方向:低(右)->高(左)) // '/0' '6' '5' '4'(ptr) '3' '2' '1'(p) '' '' '' ''(s) // char p[] ="123456";//char s[4]; // '/0' '6' '5' '4'(ptr) '/0' '6' '5'(p) '4' '3' '2' '1'(s) //strcpy(s, p); // output : 56 -- 123456 --- 456 // 解析:在這里我們可以知道p=s+4; 然后我們對s進行寫入"123456" s所在的四個字節不夠用 所以"56"(包括后面的/0)均被寫入了p地址 因此輸出p將輸出56

 結果討論:

     關於棧:三個特性:
     1. 函數棧內存以一個地址即四個字節對齊。
     2. 地址從高地址到低地址分配。
     3. 數據從低位地址到高位地址寫入。

 例子2 ----------------------------------------------------------------------------

#include<iostream>
using namespace std;

int main()
{
     char p[] ="123456";
     int c=0;
     char s[4];
     strcpy(s, p);
     cout<<p<<endl;
     cout<<c<<endl;
     return 0;
}

// output :

  123456
  13877
  請按任意鍵繼續. . .

 
內存跟蹤:
               strcpy前:
               
               
               strcpy后:
               
               
結果討論:
               這里和前面不同的地方是在p和s之間插入了一個c  它們的地址大小順序為: s<c<p 因此對s的寫入將覆蓋c而不會影響到p
               那么c的值是怎么得到的  跟蹤地址可以知道 c原本表示為: 00 00 00 00 在對s寫入"123456"后可以得到c值表為:35 36 00 00
                由於操作系統的小端表示 ,在c的四個字節里 高地址(右邊)存放數值的高位 低地址(左邊)存放地址的低位  所以c實際的數值擺放為
               00 00 36 35 轉換成十進制 得到13877
 

例子3 ----------------------------------------------------------------------------

堆的分配規則和棧不一樣  堆的地址是從小到大分配的 並且連續分配的兩個內存塊的起始地址是肯定不同的
     
     寫在前面: 關於內存所謂的空閑區值:
                    經常寫代碼忘了初始化的人可能會注意到,每次定義一個整形變量沒有初始化直接使用  往往每次都得到一個很大的負數  不是說沒有分配的內存都是隨機的么?看這個示例:
    int main()
     {
          int a;
          int* pb = new int;

          cout<<"a is from stack, b is from heap"<<endl;
          cout<<"(int)a= "<<a<<endl;
          cout<<"(int)b= "<<*pb<<endl;
    
          cout<<"(hex)a= "<<hex<<a<<endl;
          cout<<"(hex)b= "<<hex<<*pb<<endl;
          char* pa = (char*)&a;
          cout<<"(char)a= "<<pa<<endl;
          cout<<"(char)b= "<<(char*)pb<<endl;
    
          delete pb;
          return 0;
     }

 

     vc++6.0輸出結果:
     
     我相信這里面出現的這兩個字大家都很熟悉了,就連上面的兩個負整數應該都是異常眼熟吧  接下來  揭開謎底:
      對於棧和堆的空閑區(未被覆蓋前) 是會被默認初始化的, 棧內存初始化為CC,堆初始化為CD。  初看起來了解這個意義不大  但這是下面所說的一些東西的基礎。
     示例2:
     int main()
     {
          int*a = new int;
          int*b = new int;
          int*c = new int;

          delete a;
          delete b;
          delete c;
     
          return 0;
     }

 

     調試 得到
     a = 00382a48
     b = 00382a90
     c = 00382ad8

 

     內存跟蹤:
     
     這里面包含了很多信息
     1.b指針指向的地址處(00382a90) 被默認初始化為CD(堆未被覆蓋前)
     2.空閑區兩頭是有四個字節的 界定符的 這里是FD FD FD FD
     3.繼續往上看  00382a70處  有一個地址 00382a28
     4.下一個地址 00382a74處 有一個地址00383ab8
     在這里我們不妨假設這兩個地址就是已經被分配的內存的下一塊和上一塊
     接下來  我們對 00382a28進行跟蹤:
     
     在該地址下面不遠處 我們找到了00382a48 也就是指針a指向的地址  那么到了這里我們可以看出:
     堆中每一個內存塊是被連接到一個鏈表上的  並且每一個空閑區都被封在一個結構體中
      這個結構體至少應該包含:
      1. 該數據塊的前驅和后繼
      2. 該數據塊大小
      3. 數據塊的界定符
      4. 數據塊本身
 
     在這里我們可以看出 放在數據塊前面的數據區大小=00382a48-00382a28=32個字節
  (

    00382a48 - 00382a28 = 32Byte

    48(H) = 0100 1000(B) = 72(D)
    28(H) = 0010 1000(B) = 40(D)
    72 - 40 = 32

  )
      同時對其中另一部分數據進行分析 初步可以猜測:
      00382a38  分配的元素類型所占字節數 這里是sizeof(int)。
      00382a3c 分配該類型元素的個數。
 
     到這里也可以明白為什么動態分配一個數組int* p=new int[5] 只需要delete []p 就可以了 因為結構體保存了元素個數
 
     隨便再說一下  對於CD,CC只針對於還未使用過的內存  比如像棧這種動態收縮的結構  同一個內存不同時刻會存放不同的變量  在使用后回收時 只改變棧頂指針  數據是還在的  所以不是CC。

 


免責聲明!

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



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