c/c++中記憶體分配
在任何程式設計環境及語言中,記憶體管理都十分重要。在目前的計算機系統或嵌入式系統中,記憶體資源仍然是有限的。因此在程式設計中,有效地管理記憶體資源是程式設計師首先考慮的問題。
第1節主要介紹記憶體管理基本概念,重點介紹C程式中記憶體的分配,以及C語言編譯後的可執行程式的儲存結構和執行結構,同時還介紹了堆空間和棧空間的用途及區別。
第2節主要介紹C語言中記憶體分配及釋放函式、函式的功能,以及如何呼叫這些函式申請/釋放記憶體空間及其注意事項。
3.1 記憶體管理基本概念
3.1.1 C程式記憶體分配
1.C程式結構
下面列出C語言可執行程式的基本情況(Linux 2.6環境/GCC4.0)。
[[email protected] |
可以看出,此可執行程式在儲存時(沒有調入到記憶體)分為程式碼區(text)、資料區(data)和未初始化資料區(bss)3個部分。
(1)程式碼區(text segment)。存放CPU執行的機器指令(machine instructions)。通常,程式碼區是可共享的(即另外的執行程式可以呼叫它),因為對於頻繁被執行的程式,只需要在記憶體中有一份程式碼即可。程式碼區通常是隻讀的,使其只讀的原因是防止程式意外地修改了它的指令。另外,程式碼區還規劃了局部變數的相關資訊。
(2)全域性初始化資料區/靜態資料區(initialized data segment/data segment)。該區包含了在程式中明確被初始化的全域性變數、靜態變數(包括全域性靜態變數和區域性靜態變數)和常量資料(如字串常量)。例如,一個不在任何函式內的宣告(全域性資料):
int maxcount = 99;
|
使得變數maxcount根據其初始值被儲存到初始化資料區中。
static mincount=100;
|
這聲明瞭一個靜態資料,如果是在任何函式體外宣告,則表示其為一個全域性靜態變數,如果在函式體內(區域性),則表示其為一個區域性靜態變數。另外,如果在函式名前加上static,則表示此函式只能在當前檔案中被呼叫。
(3)未初始化資料區。亦稱BSS區(uninitialized data segment),存入的是全域性未初始化變數。BSS這個叫法是根據一個早期的彙編運算子而來,這個彙編運算子標誌著一個塊的開始。BSS區的資料在程式開始執行之前被核心初始化為0或者空指標(NULL)。例如一個不在任何函式內的宣告:
long sum[1000];
|
將變數sum儲存到未初始化資料區。
圖3-1所示為可執行程式碼儲存時結構和執行時結構的對照圖。一個正在執行著的C編譯程式佔用的記憶體分為程式碼區、初始化資料區、未初始化資料區、堆區和棧區5個部分。
(點選檢視大圖)圖3-1 C程式的記憶體佈局 |
(1)程式碼區(text segment)。程式碼區指令根據程式設計流程依次執行,對於順序指令,則只會執行一次(每個程序),如果反覆,則需要使用跳轉指令,如果進行遞迴,則需要藉助棧來實現。
程式碼區的指令中包括操作碼和要操作的物件(或物件地址引用)。如果是立即數(即具體的數值,如5),將直接包含在程式碼中;如果是區域性資料,將在棧區分配空間,然後引用該資料地址;如果是BSS區和資料區,在程式碼中同樣將引用該資料地址。
(2)全域性初始化資料區/靜態資料區(Data Segment)。只初始化一次。
(3)未初始化資料區(BSS)。在執行時改變其值。
(4)棧區(stack)。由編譯器自動分配釋放,存放函式的引數值、區域性變數的值等。其操作方式類似於資料結構中的棧。每當一個函式被呼叫,該函式返回地址和一些關於呼叫的資訊,比如某些暫存器的內容,被儲存到棧區。然後這個被呼叫的函式再為它的自動變數和臨時變數在棧區上分配空間,這就是C實現函式遞迴呼叫的方法。每執行一次遞迴函式呼叫,一個新的棧框架就會被使用,這樣這個新例項棧裡的變數就不會和該函式的另一個例項棧裡面的變數混淆。
(5)堆區(heap)。用於動態記憶體分配。堆在記憶體中位於bss區和棧區之間。一般由程式設計師分配和釋放,若程式設計師不釋放,程式結束時有可能由OS回收。
之所以分成這麼多個區域,主要基於以下考慮:
一個程序在執行過程中,程式碼是根據流程依次執行的,只需要訪問一次,當然跳轉和遞迴有可能使程式碼執行多次,而資料一般都需要訪問多次,因此單獨開闢空間以方便訪問和節約空間。
臨時資料及需要再次使用的程式碼在執行時放入棧區中,生命週期短。
全域性資料和靜態資料有可能在整個程式執行過程中都需要訪問,因此單獨儲存管理。
堆區由使用者自由分配,以便管理。
下面通過一段簡單的程式碼來檢視C程式執行時的記憶體分配情況。相關資料在執行時的位置如註釋所述。
//main.cpp
|
2.記憶體分配方式
在C語言中,物件可以使用靜態或動態的方式分配記憶體空間。
靜態分配:編譯器在處理程式原始碼時分配。
動態分配:程式在執行時呼叫malloc庫函式申請分配。
靜態記憶體分配是在程式執行之前進行的因而效率比較高,而動態記憶體分配則可以靈活的處理未知數目的。
靜態與動態記憶體分配的主要區別如下:
靜態物件是有名字的變數,可以直接對其進行操作;動態物件是沒有名字的變數,需要通過指標間接地對它進行操作。
靜態物件的分配與釋放由編譯器自動處理;動態物件的分配與釋放必須由程式設計師顯式地管理,它通過malloc()和free兩個函式(C++中為new和delete運算子)來完成。
以下是採用靜態分配方式的例子。
int a=100;
|
此行程式碼指示編譯器分配足夠的儲存區以存放一個整型值,該儲存區與名字a相關聯,並用數值100初始化該儲存區。
以下是採用動態分配方式的例子。
p1 = (char *)malloc(10*sizeof(int));//分配得來得10*4位元組的區域在堆區
|
此行程式碼分配了10個int型別的物件,然後返回物件在記憶體中的地址,接著這個地址被用來初始化指標物件p1,對於動態分配的記憶體唯一的訪問方式是通過指標間接地訪問,其釋放方法為:
free(p1);
|
3.1.2 棧和堆的區別
前面已經介紹過,棧是由編譯器在需要時分配的,不需要時自動清除的變數儲存區。裡面的變數通常是區域性變數、函式引數等。堆是由malloc()函式(C++語言為new運算子)分配的記憶體塊,記憶體釋放由程式設計師手動控制,在C語言為free函式完成(C++中為delete)。棧和堆的主要區別有以下幾點:
(1)管理方式不同。
棧編譯器自動管理,無需程式設計師手工控制;而堆空間的申請釋放工作由程式設計師控制,容易產生記憶體洩漏。
(2)空間大小不同。
棧是向低地址擴充套件的資料結構,是一塊連續的記憶體區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,當申請的空間超過棧的剩餘空間時,將提示溢位。因此,使用者能從棧獲得的空間較小。
堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。因為系統是用連結串列來儲存空閒記憶體地址的,且連結串列的遍歷方向是由低地址向高地址。由此可見,堆獲得的空間較靈活,也較大。棧中元素都是一一對應的,不會存在一個記憶體塊從棧中間彈出的情況。
(3)是否產生碎片。
對於堆來講,頻繁的malloc/free(new/delete)勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低(雖然程式在退出後作業系統會對記憶體進行回收管理)。對於棧來講,則不會存在這個問題。
(4)增長方向不同。
堆的增長方向是向上的,即向著記憶體地址增加的方向;棧的增長方向是向下的,即向著記憶體地址減小的方向。
(5)分配方式不同。
堆都是程式中由malloc()函式動態申請分配並由free()函式釋放的;棧的分配和釋放是由編譯器完成的,棧的動態分配由alloca()函式完成,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行申請和釋放的,無需手工實現。
(6)分配效率不同。
棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行。堆則是C函式庫提供的,它的機制很複雜,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/作業系統)在堆記憶體中搜索可用的足夠大的空間,如果沒有足夠大的空間(可能是由於記憶體碎片太多),就有需要作業系統來重新整理記憶體空間,這樣就有機會分到足夠大小的記憶體,然後返回。顯然,堆的效率比棧要低得多。
(7)如果從堆中獲取的記憶體在使用完之後沒有被釋放,這部分記憶體在程式結束之前會一直被佔用,這種情況就被稱為“記憶體洩漏”,當然我們記憶體洩漏如果在一個迴圈中,那麼堆中的記憶體很快就會被用光,它分配的方式也是按一種演算法進行的(首次適應演算法,迴圈首次適應演算法,最佳適應演算法,最壞適應演算法,快速適應演算法)
當然棧中存在的問題就是:棧溢位
(8)棧是存放程式中的所有區域性變數,函式引數,函式返回值等資訊的一塊記憶體區域。棧的記憶體管理嚴格遵循後進先出(LIFO,last in,fast out)的順序,即釋放棧中物件所佔記憶體時的順序剛好與給這些物件分配棧中記憶體時的順序相反,這一點正是實現函式呼叫所需要的。從棧中分配記憶體效率特別高,對棧的充分利用是C、C++編譯程式能產生優質高效程式碼的原因之一。堆,使用C,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. */
|
1.char資料型別
char型別資料所佔記憶體空間為8位。其中有符號字元型變數取值範圍為?128~127,無符號型字元變數取值範圍為0~255。其限制如下:
/* Number of bits in a 'char'. */
|
2.short int資料型別
short int型別資料所佔記憶體空間為16位。其中有符號短整型變數取值範圍為?32768~32767,無符號短整型變數取值範圍為0~65535。其限制如下:
/* Minimum and maximum values a 'signed short int' can hold. */ // 有符號短整型範圍
|
3.int資料型別
int型別資料所佔記憶體空間為32位。其中有符號整型變數取值範圍為?2147483648~2147483647,無符號型整型變數取值範圍為0~4294967295U。其限制如下:
/* Minimum and maximum values a 'signed int' can hold. */ //整形範圍
|
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
|
在64位機器上,如果__WORDSIZE的值為64, long int型別資料所佔記憶體空間為64位。其中有長整型變數取值範圍為-9223372036854775808L~3372036854775807L,無符號長整型變數取值範圍為0~18446744073709551615UL。其限制如下:
/* Minimum and maximum values a 'signed long int' can hold. */ //有符號長整形範圍
|
5.long long int資料型別
在C99中,還定義了long long int資料型別。其資料型別限制如下:
# ifdef __USE_ISOC99
|
3.1.4 資料儲存區域例項
此程式顯示了資料儲存區域例項,在此程式中,使用了etext、edata和end3個外部全域性變數,這是與使用者程序相關的虛擬地址。
在程式原始碼中列出了各資料的儲存位置,同時在程式執行時顯示了各資料的執行位置,圖3-2所示為程式執行過程中各變數的儲存位置。
圖3-2 函式執行時各資料位置 |
主函式原始碼如下:
[[email protected] linux_app]# cat mem_add.c
extern void afunc(void);
int bss_var; //未初始化全域性資料儲存在BSS區
int main(int argc,char *argv[])
|
子函式原始碼如下:
void afunc(void)
|
函式執行結果如下:
[[email protected] linux_app]# gcc -o mem_add mem_add.c //編譯
text Location:
bss Location:
data location:
Stack Locations:
Heap Locations:
b and nb in Stack
|
如果執行環境不一樣,執行程式的地址與此將有差異,但是,各區域之間的相對關係不會發生變化。可以通過readelf命令來檢視可執行檔案的詳細內容。
[[email protected] yangzongde]# readelf -a memadd
|
3.2 記憶體管理函式
3.2.1 malloc/free函式
Malloc()函式用來在堆中申請記憶體空間,free()函式釋放原先申請的記憶體空間。Malloc()函式是在記憶體的動態儲存區中分配一個長度為size位元組的連續空間。其引數是一個無符號整型數,返回一個指向所分配的連續儲存域的起始地址的指標。當函式未能成功分配儲存空間時(如記憶體不足)則返回一個NULL指標。
由於記憶體區域總是有限的,不能無限制地分配下去,而且程式應儘量節省資源,所以當分配的記憶體區域不用時,則要釋放它,以便其他的變數或程式使用。
這兩個函式的庫標頭檔案為:
#include
|
函式定義如下:
void *malloc(size_t size) //返回型別為空指標型別
|
例如:
int *p1,*p2;
|
malloc()函式返回值賦給p1,又把p1的值賦給p2,所以此時p1,p2都可作為free函式的引數。使用free()函式時,需要特別注意下面幾點:
(1)呼叫free()釋放記憶體後,不能再去訪問被釋放的記憶體空間。記憶體被釋放後,很有可能該指標仍然指向該記憶體單元,但這塊記憶體已經不再屬於原來的應用程式,此時的指標為懸掛指標(可以賦值為NULL)。
(2)不能兩次釋放相同的指標。因為釋放記憶體空間後,該空間就交給了記憶體分配子程式,再次釋放記憶體空間會導致錯誤。也不能用free來釋放非malloc()、calloc()和realloc()函式建立的指標空間,在程式設計時,也不要將指標進行自加操作,使其指向動態分配的記憶體空間中間的某個位置,然後直接釋放,這樣也有可能引起錯誤。
(3)在進行C語言程式開發中,malloc/free是配套使用的,即不需要的記憶體空間都需要釋放回收。
下面是使用這兩個函式的一個例子。
[[email protected] yangzongde]# cat malloc_example.c
|
在以上程式中,(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運算子
|
下面是一段C++程式程式碼:
void UseNewDelete(void)
|
下面詳細介紹C++中new/delete運算子的使用方法。
class A
|
其中,語句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<stdlib.h>
|
當呼叫realloc()函式重新分配記憶體時,如果申請失敗,將返回NULL,此時原來指標仍然有效,因此在程式編寫時需要進行判斷,如果呼叫成功,realloc()函式會重新分配一塊新記憶體,並將原來的資料拷貝到新位置,返回新記憶體的指標,而釋放掉原來指標(realloc()函式的引數指標)指向的空間,原來的指標變為不可用(即不需要再釋放,也不能再釋放),因此,一般不使用以下語句:
ptr=realloc(ptr,new_amount)
|
如果記憶體減少,malloc僅僅改變索引資訊,但並不代表被減少的部分還可以訪問,這一部分記憶體將交給系統記憶體分配子程式。
下面是一個使用relloc函式的例項。
[[email protected] yangzongde]# cat realloc_example.c
if((numbers2=(int *)malloc(5*sizeof(int)))==NULL) //(2)numbers2指標申請空間
printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n");//(4)新申請空間大小
for(n=0;n<5;n++) //(6)這5個數是從numbers2拷貝而來
for(n=0;n<input;n++) //(7)新資料初始化 |