C/C++ 內存分配情況

一、C語言中的內存地址分配模型如

 

  C/C++ <wbr>內存分配情況

1、程序代碼區:存放函數體的二進制代碼。  

2、全局區數據區:全局數據區劃分爲三個區域。全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。常量數據存放在另一個區域裏。這些數據在程序結束後由系統釋放。我們所說的BSS段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。

3、棧區:由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

4、堆區:一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。

5、命令行參數區:存放命令行參數和環境變量的值。   
   關於局部的字符串常量是存放在全局的常量區還是棧區,不同的編譯器有不同的實現。可以通過彙編語言察看一下。不過vc環境下,局部常量就像局部變量一樣存儲於棧中,全局常量、字符常量存儲於文字常量區。TC在常量區。

    在linux下:可以通過參數-c來編譯生成彙編文件。如:  
    gcc -c *.c
    gcc *.o -Map test.txt -o test.elf
    用文本編輯器查看test.txt文件,你就看到那些bss段,data段,text段等信息了,但是沒有堆棧段相關信息,用objdump命令查看.o文件的反彙編後的信息,或者用gcc -S *.c,查看各個.S文件就明白了。

 

C語言程序編譯的內存分配:
1.
棧區(stack) --編譯器自動分配釋放,主要存放函數的參數值,局部變量值等;
2.
堆區(heap) --由程序員分配釋放;
3.
全局區或靜態區 --存放全局變量和靜態變量;程序結束時由系統釋放,分爲全局初始化區和全局未初始化區;
4.
字符常量區 --常量字符串放與此,程序結束時由系統釋放;
5.
程序代碼區
例: //main.c
int a=0; //
全局初始化區
char *p1; //
全局未初始化區
void main()
{
int b; //

char s[]="bb"; //
char *p2; //

char *p3="123"; //
其中,“123\ 0”常量區,p3在棧區
static int c=0; //
全局區
p1=(char*)malloc(10); //10
個字節區域在堆區
strcpy(p1,"123"); //"123\0"
在常量區,編譯器可能會將它與p3所指向的"123"優化成一個地方。
}

 

堆與棧的區別  

一、預備知識―程序的內存分配

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

 

1、棧區(stack)―   由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。  

2、堆區(heap) ―   一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。

3、全局區(靜態區)(static)―,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。程序結束後由系統釋放。

4、文字常量區  ―常量字符串就是放在這裏的。程序結束後由系統釋放

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

二、例子程序

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

//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);

分配得來得10和20字節的區域就在堆區。  

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

 

二、堆和棧的理論知識  

2.1申請方式  

stack: 由系統自動分配。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中爲b開空  間。  

heap: 需要程序員自己申請,並指明大小,在c中malloc函數  

如p1 = (char *)malloc(10);

在C++中用new運算符

如p2 = (char *)malloc(10);

但是注意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小結:

堆和棧的區別可以用如下的比喻來看出: 使用棧就象我們去飯館裏吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。


二、一個典型的C程序存儲空間佈局

一個典型的C程序存儲空間佈局由以下幾個部分組成:

(正文段) CPU執行的指令部分,也就是主要的程序代碼編譯出來的結果,只讀,通常可以共享。

(初始化數據段)通常稱之爲數據段,包含了程序中需要明確賦值的變量,譬如一些初始化的全局變量等,如int a = 10,變量名和值都存放在這個段中。

(未初始化數據段)通常稱之爲BSSBlock Started by Symbol)段,包含了程序中沒有進行賦值的變量,譬如一些未初始化的全局變量,如 int a,在程序執行之前,內核會把這部分全部置爲0NULL

()自動變量以及每次函數調用時所需保存的信息放在此段中。如函數調用時要保存返回地址等。棧是從上向下分配的。

()通常在堆中進行動態存儲分配,如malloc, calloc, realloc等都從這裏面分配。堆是從下向上分配的。

通常堆頂和棧底之間的虛擬地址空間是很大的。

X86處理器上的Linux,正文段從0x08048000開始,棧底則從0xC0000000之下開始。

下圖是一個典型的C程序存儲空間的邏輯佈局:

C/C++ <wbr>內存分配情況

//main.c

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

}

 

三、c/c++語言中的內存分配──堆和棧的區別

基本知識

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

如下面的一個例子:

//main.cpp

#include <string>
#include <iostream>
using namespace std;

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);
//分配得來得10和20字節的區域就在堆區。
strcpy(p1, "123456"); //123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。

 二、變量的存儲方式可分爲“靜態存儲”和“動態存儲”兩種。

     靜態存儲變量通常是在變量定義時就分定存儲單元並一直保持不變,直至整個程序結束。動態存儲變量是在程序執行過程中,使用它時才分配存儲單元, 使用完畢立即釋放。典型的例子是函數的形式參數,在函數定義時並不給形參分配存儲單元,只是在函數被調用時,才予以分配,調用函數完畢立即釋放。如果一個函數被多次調用,則反覆地分配、 釋放形參變量的存儲單元。

三、而變量存儲類型是怎樣與內存分配相對應的呢?

     存儲類型有以下四種:自動(auto)、靜態(static)、外部(extern)、寄存器(register)。變量說明的完整形式應爲:存儲類型說明符 數據類型說明符 變量名,變量名…; 例如:static int a,b; 說明a,b爲靜態類型變量
auto char c1,c2; 說明c1,c2爲自動字符變量
static int a[5]={1,2,3,4,5}; 說明a爲靜整型數組
extern int x,y; 說明x,y爲外部整型變量

auto和register存儲類型的變量屬於動態存儲方式,而static和extern的變量屬於靜態存儲方式。

 static變量放在內存中的全局區(靜態區),在程序一開始就爲這個變量分配內存空間。全局靜態變量在全局可見,局部的靜態變量在局部可見,但在程序執行過程中一直駐留內存。

   static int a=0; //全局靜態變量

    void foo()
    {
       static int v=0;//局部靜態變量

      …
     } 

     auto變量是自動存儲類型,每次執行到定義該變量的語句塊時,都將會爲該變量在內存中產生一個新的拷貝,並對其進行初始化。實際上,如果不特別指明,局部變量的存儲類型就默認爲自動的,因此,加不加auto都可以。只有局部變量才能定義成寄存器變量 

深入分析

下面通過分析一個例子來深入分析一下:

#include <string>
#include <iostream>
using namespace std;
int a = 12;
extern double b ; //全局初始化區
char *d; //全局未初始化區
static int c = 12;

void foo(int foo1)
{
const char *foo2 = "abcsf";
static int foo3 = 12;
cout << " foo2:" << foo2 << " foo3:" << foo3 << endl;
}
main()
{
int e;                                       // 棧

char f[] = "abc";                        // "abc"在常量區,f[]在棧上
char *g;                                   //棧
char *h = "123456";                    //123456\12在常量區,h在棧上。
static int i = 12;                         //全局(靜態)初始化區
extern int j;
register int k = 12;
int *l = new int(12);//放在堆上
d = new char[112];
strcpy(d, h);                             //123456\12放在常量區,編譯器可能會將它與p3

                                               //所指向的"123456"優化成一個地方。
g=h;
e=1;
foo(12);
cout <<  " a:"<< a << "  b:"<< b
<< "  c:"<< c << "  d:"<< d
<< "  e:"<< e << "  f:"<< f
<< "  g:"<< g << "  h:"<< h
<< "  i:"<< i << "  j:"<< j
<< "  k:"<< k << "  l:"<< *l
<< endl;                                     //防止編譯器優化
delete l;
delete [] d;

 

//foo1.cpp

static int j = 12;
static float b = 1.0F;

    用"gcc -S foo.cpp foo2.cpp"命令生成彙編代碼foo.s, foo1.s,觀察每一段代碼的對應彙編程序。首先看foo1.cpp編譯後結果:

 

.file "foo1.cpp"

.globl _j                 全局變量
.data                   說明以下的定義是放在數據段上的,也即放在全局區(靜態區) 

                          (static)上
.align 4                 確保指令會在字(word)的邊緣開始,如果j是double型的將

                            爲.align 8
_j:                        j是foo.cpp文件中聲明的外部變量,在foo1.cpp中是靜態變量
.long 12                j的值爲12

.globl _j
.align 4
_b:
.long 1065353216   b的值爲1,爲什麼是這個數需要看浮點數的表示方法,如果b設爲

                            double型,則這裏需要分兩步設置.long 0 .long 1072693248 

     然後再看foo.cpp裏面的內容吧: 非靜態變量a的定義與靜態變量c的定義基本是一樣的有一點不同就是a的定義加入了一句.globl _a,表明這個變量是全局變量,而在文件中的static變量則沒有聲明爲全局變量。則變量的作用域只限於本文件
 

.globl _a
.data
.align 4
_a:
.long 12

.data
.align 4
_c:
.long 12

   而對於未初始化的變量d,則聲明如下

.globl _d
.globl _d
.bss                   說明以下的定義是放在未初始化的全局區(靜態區)(static)上
.align 4
_d:
.space 4             只分配大小爲4的空間(指針有4個字)節,但並沒有給這個變量賦值

     與我們預想的情況一樣,這裏並沒有對extern變量的聲明,因爲我們只要拿來用就可以了,當將兩個程序鏈接的時候會將幾個文件的data段,bss段,text段等等連接到一起,這時候就可以找到沒有聲明的變量了。

     下面還有一段關於foo3的定義。這說明編譯器將所有static變量都放在data段,而局部static變量則給他起一些奇怪的名字(帶有這個變量的作用域等信息)_ZZ3fooiE4foo3,來限定他們的作用域,這樣局部的static變量就只能在局部作用域中被訪問了。 

.align 4
_ZZ3fooiE4foo3:
.long 12

     接下來是text段,記錄了一組常數變量。這是在foo函數中用到的常量,編譯器實際上會找到本段代碼中的常量然後將其放在一起。

.text
LC0:
.ascii "abcsf\0"
LC1:
.ascii " foo2:\0"
LC2:
.ascii " foo3:\0"

      下面進入foo函數的代碼段。下面代碼的意思爲將當前基棧地址入棧,然後將棧頂地址(esp)存入棧底寄存器(ebp)(注意,因爲棧的結構是往小地址擴展的,大地址爲棧底,小地址爲棧頂),將當前棧頂自減24,所以當前棧內的空間就爲24,在24個字節的空間中有4個字節的 const char* 型的foo2。爲什麼要分配24個字節?這與編譯器的設置有關,gcc編譯器默認的最小棧空間大小是24字節。

.globl __Z3fooi
.def __Z3fooi; .scl 2; .type 32; .endef
__Z3fooi:
LFB1:
pushl �p
LCFI0:
movl %esp, �p
LCFI1:
subl $24, %esp

     下面一句話將LC0放入ebp-4的地址中(即棧底基地址-4),這裏就可以發現常量數據不可更改的性質實際上是由編譯器來決定的,當形成彙編語言之後常量型數據和局部變量就沒有什麼區別了,即函數中代碼 int a =10; 與 const int b = 10; 的編譯結果都是一樣的,不同的是當在本代碼塊中改變b時編譯通不過而已。

movl $LC0, -4(�p)

      下面的幾行代碼比較難理解,將一個東西放到棧頂,然後將LC1(要輸出的第一個字符串)放到棧頂朝棧底方向+4個地址的位置,然後調用一個函數。可以想象這個函數要用到esp地址與esp+4地址中的變量,這可能是cout的一個機制。

movl $__ZSt4cout, (%esp)
movl $LC1, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

       接着將eax值放入esp,這是cout函數的返回值,這句話與movl $__ZSt4cout, (%esp)
首次用cout的作用是一樣的,返回一個流可以繼續 <<( C++ primer 裏面有詳細的說明)不用理他。再將ebp-4地址中的變量(foo2)放入esp+4,由於棧寄存器不能直接賦值所以用了一下eax,然後調用同樣的函數。
movl �x, (%esp)
movl -4(�p), �x
movl �x, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

     下面的代碼同理,不詳述。

movl �x, (%esp)
movl $LC2, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl �x, (%esp)
movl _ZZ3fooiE4foo3, �x
movl �x, 4(%esp)
call __ZNSolsEi
movl �x, (%esp)
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
call __ZNSolsEPFRSoS_E
leave
ret

      我們的話題有點偏離主題了,現在在重新回到內存分配的主題。下面又是一個很大的text段,編譯器將本段內的字符串常量都放在這裏了

.text
LC3:
.ascii "abc\0"
LC4:
.ascii "123456\0"
LC5:
.ascii " a:\0"
LC6:
.ascii "  b:\0"
LC7:
.ascii "  c:\0"
LC8:
.ascii "  d:\0"
LC9:
.ascii "  e:\0"
LC10:
.ascii "  f:\0"
LC11:
.ascii "  g:\0"
LC12:
.ascii "  h:\0"
LC13:
.ascii "  i:\0"
LC14:
.ascii "  j:\0"
LC15:
.ascii "  k:\0"
LC16:
.ascii "  l:\0"

    同樣,下面的代碼將函數塊中的static變量放入data塊中。

.data
.align 4
_ZZ4mainE1i:
.long 12

     分配40個字節的棧空間。

_main:
LFB2:
pushl �p
LCFI3:
movl %esp, �p
LCFI4:
subl $40, %esp

    下面的代碼是gcc編譯器的new的過程,new的過程需要12個字節的棧空間。總之,我們知道new的東西沒有放在棧上,只將返回的指針放在棧上。

movl $4, (%esp)                 new一個int所以是4個字節
call __Znwj                   new空間時調用的函數
movl �x, -28(�p)   返回的地址賦給l
movl -28(�p), �x
movl $12, (�x)               12賦值給這個地址,以下可能是在檢驗是否出錯
movb $0, %al
movl -28(�p), �x
movl �x, -20(�p)
testb %al, %al
je L10
movl -28(�p), �x
movl �x, (%esp)
call __ZdlPv

下面是堆上分配數組。

movl $112, (%esp)
call __Znaj
movl �x, _d
movl _d, �x
movl �x, (%esp)               這裏將指針賦給esp的原因是後面繼續要將這個指針用

                                   於strcpy

g=h

movl -16(�p), �x
movl �x, -12(�p)
e =1

movl $1, -4(�p)
movl $12, (%esp)

     上面的語句將棧空間補滿了,下面的語句實際上直接用extern變量,因爲在本代碼中沒有對_b分配內存,所以若是連接的代碼也沒有這個變量的話將會出現連接錯誤。

movl _b, �x
movl �x, 4(%esp)
call __ZNSolsEf
     在register變量中直接出現了下面的代碼,編譯器將有k出現的地方直接用12代替了。這裏大概跟宏差不多。

movl $12, 4(%esp)
call __ZNSolsEi

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章