1. 程式人生 > >程式在執行過程中記憶體的分配問題

程式在執行過程中記憶體的分配問題

3.1.2 棧和堆的區別

前面已經介紹過,棧是由編譯器在需要時分配的,不需要時自動清除的變數儲存區。裡面的變數通常是區域性變數、函式引數等。堆是由malloc()函式(C++語言為new運算子)分配的記憶體塊,記憶體釋放由程式設計師手動控制,在C語言為free函式完成(C++中為delete)。棧和堆的主要區別有以下幾點:

(1)管理方式不同。

棧編譯器自動管理,無需程式設計師手工控制;而堆空間的申請釋放工作由程式設計師控制,容易產生記憶體洩漏。

(2)空間大小不同。

棧是向低地址擴充套件的資料結構,是一塊連續的記憶體區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,當申請的空間超過棧的剩餘空間時,將提示溢位。因此,使用者能從棧獲得的空間較小。

堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。因為系統是用連結串列來儲存空閒記憶體地址的,且連結串列的遍歷方向是由低地址向高地址。由此可見,堆獲得的空間較靈活,也較大。棧中元素都是一一對應的,不會存在一個記憶體塊從棧中間彈出的情況。

(3)是否產生碎片。

對於堆來講,頻繁的malloc/free(new/delete)勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低(雖然程式在退出後作業系統會對記憶體進行回收管理)。對於棧來講,則不會存在這個問題。

(4)增長方向不同。

堆的增長方向是向上的,即向著記憶體地址增加的方向;棧的增長方向是向下的,即向著記憶體地址減小的方向。

(5)分配方式不同。

堆都是程式中由malloc()函式動態申請分配並由free()函式釋放的;棧的分配和釋放是由編譯器完成的,棧的動態分配由alloca()函式完成,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行申請和釋放的,無需手工實現。

(6)分配效率不同。

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

3.1.3 Linux資料型別大小

在Linux作業系統下使用GCC進行程式設計,目前一般的處理器為32位字寬,下面是/usr/include/limit.h檔案對Linux下資料型別的限制及儲存位元組大小的說明。

/* We don't have #include_next.   Define ANSI  for standard 32-bit words.  */
/* These assume 8-bit 'char's, 16-bit 'short int's,   and 32-bit 'int's and 'long int's.  */

1.char資料型別

char型別資料所佔記憶體空間為8位。其中有符號字元型變數取值範圍為?128~127,無符號型字元變數取值範圍為0~255。其限制如下:

/* Number of bits in a 'char'. */
#  define CHAR_BIT 8          //所佔位元組數
/* Minimum and maximum values a 'signed char' can hold.  */  //有符號字元型範圍
#  define SCHAR_MIN (-128)
#  define SCHAR_MAX 127
/* Maximum value an 'unsigned char' can hold.  (Minimum is 0.)  */ //無符號字元型範圍
#  define UCHAR_MAX 255
/* Minimum and maximum values a 'char' can hold.  */
#  ifdef __CHAR_UNSIGNED__
#   define CHAR_MIN 0
#   define CHAR_MAX UCHAR_MAX
#  else
#   define CHAR_MIN SCHAR_MIN
#   define CHAR_MAX SCHAR_MAX
#  endif

2.short int資料型別

short int型別資料所佔記憶體空間為16位。其中有符號短整型變數取值範圍為?32768~32767,無符號短整型變數取值範圍為0~65535。其限制如下:

/* Minimum and maximum values a 'signed short int' can hold.  */ // 有符號短整型範圍
#  define SHRT_MIN (-32768)
#  define SHRT_MAX 32767
/* Maximum value an 'unsigned short int' can hold.  (Minimum is 0.)  */// 無符號短整型範圍
#  define USHRT_MAX 65535

3.int資料型別

int型別資料所佔記憶體空間為32位。其中有符號整型變數取值範圍為?2147483648~2147483647,無符號型整型變數取值範圍為0~4294967295U。其限制如下:

/* Minimum and maximum values a 'signed int' can hold.  */  //整形範圍
#  define INT_MIN (-INT_MAX - 1)     
#  define INT_MAX 2147483647
/* Maximum value an 'unsigned int' can hold.  (Minimum is 0.)  */ //無符號整形範圍
#  define UINT_MAX 4294967295U

4.long int資料型別

隨著巨集__WORDSIZE值的改變,long int資料型別的大小也會發生改變。如果__WORDSIZE的值為32,則long int和int型別一樣,佔有32位。在Linux GCC4.0-i386版本中,預設情況下__WORDSIZE的值為32。其定義如下:

//come from /usr/include/bits/wordsize.h
#define __WORDSIZE 32

在64位機器上,如果__WORDSIZE的值為64, long int型別資料所佔記憶體空間為64位。其中有長整型變數取值範圍為-9223372036854775808L~3372036854775807L,無符號長整型變數取值範圍為0~18446744073709551615UL。其限制如下:

/* Minimum and maximum values a 'signed long int' can hold.  */ //有符號長整形範圍
#  if __WORDSIZE == 64
#   define LONG_MAX 9223372036854775807L
#  else
#   define LONG_MAX 2147483647L
#  endif
#  define LONG_MIN (-LONG_MAX - 1L)
/* Maximum value an 'unsigned long int' can hold.  (Minimum is 0.)  *///無符號長整形範圍
#  if __WORDSIZE == 64
#   define ULONG_MAX 18446744073709551615UL
#  else
#   define ULONG_MAX 4294967295UL
#  endif

5.long long int資料型別

在C99中,還定義了long long int資料型別。其資料型別限制如下:

#  ifdef __USE_ISOC99
/* Minimum and maximum values a 'signed long long int' can hold.  *///無符號長長整形範圍
#   define LLONG_MAX 9223372036854775807LL
#   define LLONG_MIN (-LLONG_MAX - 1LL)
/* Maximum value an 'unsigned long long int' can hold.  (Minimum is 0.)  *///有符號長長整形範圍
#   define ULLONG_MAX 18446744073709551615ULL
#  endif /* ISO C99 */

3.1.4 資料儲存區域例項

此程式顯示了資料儲存區域例項,在此程式中,使用了etext、edata和end3個外部全域性變數,這是與使用者程序相關的虛擬地址。

在程式原始碼中列出了各資料的儲存位置,同時在程式執行時顯示了各資料的執行位置,圖3-2所示為程式執行過程中各變數的儲存位置。

圖3-2 函式執行時各資料位置

主函式原始碼如下:

[root@localhost linux_app]# cat mem_add.c
#include
#include
#include
#include
extern void afunc(void);
extern etext,edata,end;
int bss_var;                                //未初始化全域性資料儲存在BSS區
int data_var=42;                            //初始化全域性資料儲存在資料區
#define SHW_ADR(ID,I) printf("the %8s\t is at adr:%8x\n",ID,&I); //列印地址巨集
int main(int argc,char *argv[])
{
char *p,*b,*nb;
printf("Adr etext:%8x\t Adr edata %8x\t Adr end %8x\t\n",&etext,&edata,&end);
printf("\ntext Location:\n");
SHW_ADR("main",main);              //檢視程式碼段main函式位置
SHW_ADR("afunc",afunc);           //檢視程式碼段afunc函式位置
printf("\nbss Location:\n");
SHW_ADR("bss_var",bss_var);      /檢視BSS段變數位置
printf("\ndata location:\n");
SHW_ADR("data_var",data_var);     /檢視資料段變數
printf("\nStack Locations:\n"); 
afunc();
p=(char *)alloca(32);              //從棧中分配空間
if(p!=NULL)
{
SHW_ADR("start",p);
SHW_ADR("end",p+31);
}
b=(char *)malloc(32*sizeof(char));   //從堆中分配空間
nb=(char *)malloc(16*sizeof(char));  //從堆中分配空間
printf("\nHeap Locations:\n");
printf("the Heap start: %p\n",b);   //堆起始位置
printf("the Heap end:%p\n",(nb+16*sizeof(char)));//堆結束位置
printf("\nb and nb in Stack\n");
SHW_ADR("b",b);       //顯示棧中資料b的位置
SHW_ADR("nb",nb);       //顯示棧中資料nb的位置
free(b);         //釋放申請的空間,以避免記憶體洩漏
free(nb);         //釋放申請的空間,以避免記憶體洩漏
}

子函式原始碼如下:

void afunc(void)
{
static int long level=0;          //靜態資料儲存在資料段中
int      stack_var;                 //區域性變數,儲存在棧區
if(++level==5)
{
return;
}
printf("stack_var is at:%p\n",&stack_var);
//      SHW_ADR("stack_var in stack section",stack_var);
//      SHW_ADR("Level in data section",level);
afunc();
}

函式執行結果如下:

[root@localhost linux_app]# gcc -o mem_add mem_add.c //編譯
[root@localhost linux_app]# ./mem_add     //執行結果
Adr etext: 8048702       Adr edata  8049950      Adr end  804995c
text Location:
the     main     is at adr: 8048418
the    afunc     is at adr: 8048611
bss Location:
the  bss_var     is at adr: 8049958
data location:
the data_var     is at adr: 804994c
Stack Locations:
the stack_var in stack section    is at adr:bfbf6c44
the Level in data section         is at adr: 8049954
the stack_var in stack section    is at adr:bfbf6c24
the Level in data section         is at adr: 8049954
the stack_var in stack section    is at adr:bfbf6c04
the Level in data section         is at adr: 8049954
the stack_var in stack section    is at adr:bfbf6be4
the Level in data section         is at adr: 8049954
the    start     is at adr:bfbf6c74
the      end     is at adr:bfbf6cf0
Heap Locations:
the Heap start: 0x8453008
the Heap end:0x8453040
b and nb in Stack
the        b     is at adr:bfbf6c70
the       nb     is at adr:bfbf6c6c

如果執行環境不一樣,執行程式的地址與此將有差異,但是,各區域之間的相對關係不會發生變化。可以通過readelf命令來檢視可執行檔案的詳細內容。

[root@localhost yangzongde]# readelf -a memadd

3.2 記憶體管理函式

3.2.1 malloc/free函式

Malloc()函式用來在堆中申請記憶體空間,free()函式釋放原先申請的記憶體空間。Malloc()函式是在記憶體的動態儲存區中分配一個長度為size位元組的連續空間。其引數是一個無符號整型數,返回一個指向所分配的連續儲存域的起始地址的指標。當函式未能成功分配儲存空間時(如記憶體不足)則返回一個NULL指標。

由於記憶體區域總是有限的,不能無限制地分配下去,而且程式應儘量節省資源,所以當分配的記憶體區域不用時,則要釋放它,以便其他的變數或程式使用。

這兩個函式的庫標頭檔案為:

#include

函式定義如下:

void *malloc(size_t size)   //返回型別為空指標型別
void free(void *ptr)

例如:

int *p1,*p2;
p1=(int *)malloc(10*sizeof(int));
p2=p1;
……
free(p2) ;      /*或者free(p1)*/
p1=NULL;       /*或者p2=NULL */

malloc()函式返回值賦給p1,又把p1的值賦給p2,所以此時p1,p2都可作為free函式的引數。使用free()函式時,需要特別注意下面幾點:

(1)呼叫free()釋放記憶體後,不能再去訪問被釋放的記憶體空間。記憶體被釋放後,很有可能該指標仍然指向該記憶體單元,但這塊記憶體已經不再屬於原來的應用程式,此時的指標為懸掛指標(可以賦值為NULL)。

(2)不能兩次釋放相同的指標。因為釋放記憶體空間後,該空間就交給了記憶體分配子程式,再次釋放記憶體空間會導致錯誤。也不能用free來釋放非malloc()、calloc()和realloc()函式建立的指標空間,在程式設計時,也不要將指標進行自加操作,使其指向動態分配的記憶體空間中間的某個位置,然後直接釋放,這樣也有可能引起錯誤。

(3)在進行C語言程式開發中,malloc/free是配套使用的,即不需要的記憶體空間都需要釋放回收。

下面是使用這兩個函式的一個例子。

[root@localhost yangzongde]# cat malloc_example.c 
#include               //printf()    //(1)標頭檔案資訊
#include              //malloc()    //(2)
int main(int argc,char* argv[],char* envp[])   //(3)
{
int count;
int* array;
if((array=(int *)malloc(10*sizeof(int)))==NULL)  //(4)分配空間
{
printf("malloc memory unsuccessful");
exit(1);
}
for (count=0;count<10;count++)      //(5) 賦值
{
*array=count;
array++;
}
for(count=9;count>=0;count--)                  //(6)賦值
{
array--;
printf("%4d",*array);
}
printf("\n");
free(array);        //(7)釋放空間
array=NULL;       //(8)將指標置為空,避免不安全訪問
exit (0);
}
[root@localhost yangzongde]# gcc -o malloc_example malloc_example.c  //編譯
[root@localhost yangzongde]# ./malloc_example       //執行
9   8   7   6   5   4   3   2   1   0

在以上程式中,(1)句中包含stdio.h標頭檔案,從而在後面可以呼叫printf()函式。(2)句中包含stdlib.h標頭檔案,其是malloc()函式的標頭檔案。(3)句為函式的入口位置,此處採用Linux下程式設計標準,返回值為int型,argc為引數個數, argv[]為引數,envp[]存放的是所有環境變數。(4)句動態分配了10個整型儲存區域,此語句可以分為以下幾步。

① 分配10個整型的連續儲存空間,並返回一個指向其起始地址的整型指標。

② 把此整型指標地址賦給array。

③ 檢測返回值是否為NULL。

(5)、(6)句為陣列賦值並列印輸出,以免記憶體洩漏。(7)句呼叫free()函式釋放記憶體空間。(8)句將一個NULL指標傳遞給array,雖然在很多情況下可以不用此句,但這樣處理可以避免此指標成為野指標。

在C++中,使用new和delete運算子來實現記憶體的分配和釋放,使用new/delete運算子實現記憶體管理比使用malloc/free函式更有優越性。new/delete運算子定義如下:

static void* operator new(size_t sz);     //new運算子
static void  operator delete(void* p);      //delete運算子

下面是一段C++程式程式碼:

void UseNewDelete(void)
{
Obj  *a = new Obj;           //申請動態記憶體並且初始化
//…
delete a;                   //清除並且釋放記憶體
}

下面詳細介紹C++中new/delete運算子的使用方法。

class A
{
public:
A()  {   cout<<"A is here!"<~A() {   cout<<"A is dead!"<private:
int i;
};
A* pA=new A;     //呼叫new運算子申請空間
delete pA;      //刪除pA

其中,語句new A完成了以下兩個功能:

(1)呼叫運算子new,在自由儲存區分配一個sizeof(A)大小的記憶體空間。

(2)呼叫建構函式A(),在這塊記憶體空間上初始化物件。

當然,delete pA完成相反的兩件事:

(1)呼叫解構函式~A(),銷燬物件。

(2)呼叫運算子delete,釋放記憶體。

由此可以看出,運算子new和delete提供了動態分配和釋放儲存區的功能。它們的作用相當於C語言的malloc()和free()函式,但是效能更為優越。使用new比使用malloc()有以下幾個優點:

(1)new自動計算要分配給物件的記憶體空間大小,不使用sizeof運算子,簡單,而且可以避免錯誤。

(2)自動地返回正確的指標型別,不用進行強制型別轉換。

(3)用建構函式給分配的物件進行初始化。

但是,使用malloc函式和new分配記憶體的時候,本身並沒有對這塊記憶體空間做清零等任何動作。因此,申請記憶體空間後,其返回的新分配的記憶體是沒有零填充的,程式設計師需要使用memset()函式來初始化記憶體。

3.2.2 realloc--更改已經配置的記憶體空間

realloc()函式用來從堆上分配記憶體,當需要擴大一塊記憶體空間時,realloc()試圖直接從堆上當前記憶體段後面的位元組中獲得更多的記憶體空間,如果能夠滿足,則返回原指標;如果當前記憶體段後面的空閒位元組不夠,那麼就使用堆上第一個能夠滿足這一要求的記憶體塊,將目前的資料複製到新的位置,而將原來的資料塊釋放掉。如果記憶體不足,重新申請空間失敗,則返回NULL。此函式定義如下:

void *realloc(void *ptr,size_t size)

引數ptr為先前由malloc、calloc和realloc所返回的記憶體指標,而引數size為新配置的記憶體大小。其庫標頭檔案為:

#include

當呼叫realloc()函式重新分配記憶體時,如果申請失敗,將返回NULL,此時原來指標仍然有效,因此在程式編寫時需要進行判斷,如果呼叫成功,realloc()函式會重新分配一塊新記憶體,並將原來的資料拷貝到新位置,返回新記憶體的指標,而釋放掉原來指標(realloc()函式的引數指標)指向的空間,原來的指標變為不可用(即不需要再釋放,也不能再釋放),因此,一般不使用以下語句:

ptr=realloc(ptr,new_amount)

如果記憶體減少,malloc僅僅改變索引資訊,但並不代表被減少的部分還可以訪問,這一部分記憶體將交給系統記憶體分配子程式。

下面是一個使用relloc函式的例項。

[root@localhost yangzongde]# cat realloc_example.c 
#include
#include
int main (int argc,char* argv[],char* envp[])   //(1)主函式
{
int input;
int n;
int *numbers1;
int *numbers2;
numbers1=NULL;
     if((numbers2=(int *)malloc(5*sizeof(int)))==NULL) //(2)numbers2指標申請空間
{
printf("malloc memory unsuccessful");
//free(numbers2);
numbers2=NULL;
exit(1);
}
for (n=0;n<5;n++)       //(3)初始化
{
*(numbers2+n)=n;
printf("numbers2's data: %d\n",*(numbers2+n));
}
     printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n");//(4)新申請空間大小 
scanf ("%d",&input);

numbers1=(int *)realloc(numbers2,(input+5)*sizeof(int));  //(5)重新申請空間
if (numbers1==NULL)
{
printf("Error (re)allocating memory");
exit (1);
}
     for(n=0;n<5;n++)       //(6)這5個數是從numbers2拷貝而來
{
printf("the numbers1s's data copy from numbers2: %d\n",*(numbers1+n));
}
     for(n=0;n{
*(numbers1+5+n)=n*2;
printf ("nummber1's new data: %d\n",*(numbers1+5+n)); // numbers1++;
}
printf("\n");
free(numbers1);       //(8)釋放numbers1
numbers1=NULL;
// free(numbers2);     //(9)不能再釋放numbers2
return 0;
}
[root@localhost yangzongde]# gcc -o realloc_example realloc_example.c
[root@localhost yangzongde]# ./realloc_example
numbers2's data: 0
numbers2's data: 1
numbers2's data: 2
numbers2's data: 3
numbers2's data: 4
Enter an integer value you want to remalloc ( enter 0 to stop) //重新申請空間

the numbers1s's data copy from numbers2: 0
the numbers1s's data copy from numbers2: 1
the numbers1s's data copy from numbers2: 2
the numbers1s's data copy from numbers2: 3
the numbers1s's data copy from numbers2: 4
nummber1's new data: 0
nummber1's new data: 2
nummber1's new data: 4
nummber1's new data: 6
nummber1's new data: 8

此程式是一個簡單的重新申請記憶體空間的例項,(1)為函式入口,前面已經介紹過。(2)從堆空間中申請5個int空間,將返回地址賦給numbers2,如果返回值為NULL,將返回錯誤資訊,釋放numbers2並退出。(3)為新申請的空間初始化。(4)輸入需要增加的記憶體數量。(5)呼叫realloc()函式重新申請記憶體空間,重新申請記憶體空間大小為原有空間大小加上使用者輸入的記憶體空間數。如果申請失敗,將返回NULL,此時numbers2仍然有效。如果申請成功,將重新分配一塊大小合適的空間,並將新空間首地址賦給numbers1,同時將numbers2所指向的5個空間的資料複製到新的記憶體空間中,釋放掉原來numbers2所指向的記憶體空間。(6)列印從numbers2所指向的原空間拷貝的資料,(7)句對新增加的空間進行初始化。(8)句釋放number1所指向的新申請空間。(9)為註釋掉的程式碼,提示讀者此時對原空間再次釋放,因為第(5)已經完成了這一操作。