在任何程序設計環境及語言中,內存管理都十分重要。在目前的計算機系統或嵌入式系統中,內存資源仍然是有限的。因此在程序設計中,有效地管理內存資源是程序員首先考慮的問題。
第1節主要介紹內存管理基本概念,重點介紹C程序中內存的分配,以及C語言編譯後的可執行程序的存儲結構和運行結構,同時還介紹了堆空間和棧空間的用途及區別。
第2節主要介紹C語言中內存分配及釋放函數、函數的功能,以及如何調用這些函數申請/釋放內存空間及其注意事項。
3.1 內存管理基本概念
3.1.1 C程序內存分配
1.C程序結構
下面列出C語言可執行程序的基本情況(Linux 2.6環境/GCC4.0)。
[root@localhost Ctest]# ls test -l //test爲一個可執行程序 |
可以看出,此可執行程序在存儲時(沒有調入到內存)分爲代碼區(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函數庫提供的,它的機制很複雜,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大的空間,如果沒有足夠大的空間(可能是由於內存碎片太多),就有需要操作系統來重新整理內存空間,這樣就有機會分到足夠大小的內存,然後返回。顯然,堆的效率比棧要低得多。