C++程序內存分配問題

from:http://hi.baidu.com/sxnuwhui/item/bf4b835d4dcc474b4eff20a5

一、C++程序內存分配

1) 在棧上創建。在執行函數時,函數內局部變量的存儲單元都在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置於處理器的指令集中,一般使用寄存器來存取,效率很高,但是分配的內存容量有限。

2) 從堆上分配,亦稱動態內存分配。程序在運行的時候用malloc或new申請任意多少的內存,程序員自己負責在何時用free或delete來釋放內存。動態內存的生存期由程序員自己決定,使用非常靈活。

3) 從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static變量。

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

5)程序代碼區。 存放函數體的二進制代碼。

經典實例:

Code

#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"優化成一個地方。

}

二、三種內存對象的比較

  棧對象的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程序員操心;而且棧對象的創建速度一般較堆對象快,因爲分配堆對象時,會調用operator new操作,operator new會採用某種內存空間搜索算法,而該搜索過程可能是很費時間的,產生棧對象則沒有這麼麻煩,它僅僅需要移動棧頂指針就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,所以體積比較大的對象不適合在棧中分配。特別要注意遞歸函數中最好不要使用棧對象,因爲隨着遞歸調用深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢出,這樣就會產生運行時錯誤。

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

  靜態存儲區。所有的靜態對象、全局對象都於靜態存儲區分配。關於全局對象,是在main()函數執行前就分配好了的。其實,在main()函數中的顯示代 碼執行之前,會調用一個由編譯器生成的_main()函數,而_main()函數會進行所有全局對象的的構造及初始化工作。而在main()函數結束之 前,會調用由編譯器生成的exit函數,來釋放所有的全局對象。比如下面的代碼:

void main(void)

{

… …// 顯式代碼

}


實際上,被轉化成這樣:

void main(void)

{

_main(); //隱式代碼,由編譯器產生,用以構造所有全局對象

… … // 顯式代碼

… …

exit() ; // 隱式代碼,由編譯器產生,用以釋放所有全局對象

}


  除了全局靜態對象,還有局部靜態對象通和class的靜態成員,局部靜態對象是在函數中定義的,就像棧對象一樣,只不過,其前面多了個static關鍵字。局部靜態對象的生命期是從其所在函數第一次被調用,更確切地說,是當第一次執行到該靜態對象的聲明代碼時,產生該靜態局部對象,直到整個程序結束時,才銷燬該對象。class的靜態成員的生命週期是該class的第一次調用到程序的結束。

三 函數調用與堆棧

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

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


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


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

代碼如下:

Code

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;堆棧變化如下:

第二,Add(*pi,*pj);堆棧如下:

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

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

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

1)在遞歸的時候還是多用new

2)棧內存溢出多出現在遞歸的時候棧空間不夠

3)大塊數據塊多用堆

4)每個線程有自己的棧

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

內存基本上分爲靜態存儲區、堆區和棧區三大部分。

程序運行時,特別要注意的是內存的分配。下面介紹C++程序設計中的內存分配

一、內存基本構成

可編程內存在基本上分爲這樣的幾大部分:靜態存儲區、堆區和棧區。他們的功能不同,對他們使用方式也就不同。

靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。它主要存放靜態數據、全局數據和常量。

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

堆區:亦稱動態內存分配。程序在運行的時候用malloc或new申請任意大小的內存,程序員自己負責在適當的時候用free或delete釋放內存。動態內存的生存期可以由我們決定,如果我們不釋放內存,程序將在最後才釋放掉動態內存。 但是,良好的編程習慣是:如果某動態內存不再使用,需要將其釋放掉,否則,我們認爲發生了內存泄漏現象。

二、三者之間的區別

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

例一:靜態存儲區與棧區

  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裏面保存的地址是完全相同的。換句話說,在數據區只保留一份相同的數據。

例二:棧區與堆區

  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;  

這兩個函數都是將某個存儲空間的地址返回,二者有何區別呢?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. …  

這個程序做了一件很無意義並且會帶來很大危害的事情。因爲,雖然申請了堆內存,p保存了堆內存的首地址。但是,此變量是臨時變量,當函數調用結束時p變量消失。也就是說,再也沒有變量存儲這塊堆內存的首地址,我們將永遠無法再使用那塊堆內存了。

但是,這塊堆內存卻一直標識被你所使用(因爲沒有到程序結束,你也沒有將其delete,所以這塊堆內存一直被標識擁有者是當前您的程序),進而其他進程或程序無法使用。我們將這種不道德的“流氓行爲”(我們不用,卻也不讓別人使用)稱爲內存泄漏。這是我們C++程序員的大忌!!請大家一定要避免這件事情的發生。

總之,對於堆區、棧區和靜態存儲區它們之間最大的不同在於,棧的生命週期很短暫。但是堆區和靜態存儲區的生命週期相當於與程序的生命同時存在(如果您不在程序運行中間將堆內存delete的話),我們將這種變量或數據成爲全局變量或數據。但是,對於堆區的內存空間使用更加靈活,因爲它允許你在不需要它的時候,隨時將它釋放掉,而靜態存儲區將一直存在於程序的整個生命週期中。

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

from: http://www.cnblogs.com/rusty/archive/2011/03/21/1990667.html

一、一個由C/C++編譯到程序佔用的內存分爲以下幾個部分:

1、棧區(stack)——由編譯器自動分配釋放,在不需要的時候自動清除。用於存放函數的參數、局部變量等。操作方式類似數據結構中的棧(後進先出)。

2、堆區(heap)——一般由程序員分配釋放,若程序員分配後不釋放,程序結束後可能由OS回收。不同於數據結構中的堆,分配方式有些類似鏈表。

3、全局區(靜態區)——全局變量和靜態變量存儲在這裏。程序結束後由系統釋放。在以前到C語言中,全局變量又細分爲初始化的(DATA段)和未初始化到(BSS段),在C++裏已經沒有這個區分了,它們共同佔用同一塊內存區。

4、常量存儲區——常量字符串就存放在這裏。一般不允許修改。程序結束後由系統釋放。

5、代碼區——存放函數體的二進制代碼。

示意圖如下:

|----------------------|     高地址
|     棧區(Statk)    | -->向下增長
|----------------------|
|     堆區(Heap)    | -->向上增長
|----------------------|
| 未初始化(BSS) |
|----------------------|
|   初始化(Data)   |
|----------------------|
|    常量存儲區    |

|----------------------|

|   正文段(Text)   |
|----------------------|    低地址

附上另一副圖:

二、一段經典的例子程序,幫助理解

複製代碼
1 //main.c 2 #include<string.h> 3 #include<stdlib.h> 4 int a = 0;//全局初始化區 5 char *p1; //全局未初始化區 6 int main() 7 { 8 int b = 0;// 9 char s[] = "abc";// 10 char *p2;// 11 char *p3 = "123456";//123456\0在常量區,p3在棧上 12 static int c = 0;//全局初始化區 13 p1 = (char *)malloc(10); 14 p2 = (char *)malloc(20);//分配得到到空間在堆區 15 strcpy(p1,"123456");//123456\0放在常量區 16                        //編譯器可能會將它與p3所指向的123456\0優化成一個地方 17 return 0; 18 }
複製代碼
Ubuntu下用gcc生成彙編看看,命令:
gcc -S main.c
打開目錄下到main.s,彙編代碼如下:
複製代碼
1 .file "main.c" 2 .globl a 3 .bss                   ;大概看出這是BSS段聲明 4 .align 4 5 .type a, @object 6 .size a, 4 7  a: 8 .zero 4 9 .comm p1,4,4        ;這裏大概就是DATA段 10 .section .rodata 11 .LC1: 12 .string "123456"    ;常量存儲區 13 .LC0: 14 .string "abc"       ;棧區 15 .text                  ;代碼段 16 .globl main 17 .type main, @function 18 main: 19 pushl %ebp 20 movl %esp, %ebp     ;保存esp現場在ebp中,ebp保存在棧中。Linux下mov a,b是把a賦值給b,與Win下相反 21 andl $-16, %esp     ;這個貌似是爲了對齊神馬的 22 subl $32, %esp      ;分配棧區,用於存放局部變量等。棧區向下增長,因此是減 23 movl $0, 28(%esp)   ;esp+28,自然是int b = 0;這句 24 movl .LC0, %eax 25 movl %eax, 24(%esp) ;char s[] = "abc"; 26 movl $.LC1, 16(%esp);char *p3 = "123456"這句,esp+20是p2,char *p2這句被優化到後面30-32去了 27 movl $10, (%esp) 28 call malloc 29 movl %eax, p1       ;p1 = (char *)malloc(10); 30 movl $20, (%esp) 31 call malloc 32 movl %eax, 20(%esp) ;p2 = (char *)malloc(20); 33 movl $.LC1, %edx 34 movl p1, %eax 35 movl $7, 8(%esp) 36 movl %edx, 4(%esp) 37 movl %eax, (%esp) 38 call memcpy         ;strcpy(p1,"123456")這句,“123456\0”,用的LC1,和上面用的一個 39 movl $0, %eax       ;return 0; 40 leave 41 ret 42 .size main, .-main 43 .local c.1974 44 .comm c.1974,4,4 45 .ident "GCC: (Ubuntu/Linaro 4.4.4-14ubuntu5) 4.4.5" 46 .section .note.GNU-stack,"",@progbits
複製代碼

用空再試試-o -o2神馬的。還有Win下到反彙編。

Linux下的彙編教程看這裏

三、回憶幾個題

1、常量存儲區的優化

複製代碼
1 #include<stdio.h> 2 int main() 3 { 4 char str1[] = "hello world"; 5 char str2[] = "hello world"; 6 char* str3 = "hello world"; 7 char* str4 = "hello world"; 8 if(str1 == str2) 9 printf("str1 and str2 are same.\n"); 10 else 11 printf("str1 and str2 are not same.\n"); 12 13 if(str3 == str4) 14 printf("str3 and str4 are same.\n"); 15 else 16 printf("str3 and str4 are not same.\n"); 17 return 0; 18 } //str1 and str2 are not same. //str3 and str4 are same.
複製代碼
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章