引言
在《虛擬地址空間以及編譯模式》一節中講到,虛擬地址空間在32位環境下的大小為 4GB,在64位環境下的大小為 256TB,那么,一個C語言程序的內存在整個地址空間中是如何分布的呢?數據在哪里?代碼在哪里?為什么要這樣分布?這些就是本節要講解的內容。
內存模型
程序內存在地址空間中的分布情況稱為內存模型(Memory Model)。內存模型由操作系統構建,在Linux和Windows下有所差異,並且會受到編譯模式的影響,本節我們講解Linux下32位環境和64位環境的內存模型。
內核空間和用戶空間
對於32位環境,理論上程序可以擁有 4GB 的虛擬地址空間,我們在C語言中使用到的變量、函數、字符串等都會對應內存中的一塊區域。
但是,在這 4GB 的地址空間中,要拿出一部分給操作系統內核使用,應用程序無法直接訪問這一段內存,這一部分內存地址被稱為內核空間(Kernel Space)。
Windows 在默認情況下會將高地址的 2GB 空間分配給內核(也可以配置為1GB),而 Linux 默認情況下會將高地址的 1GB 空間分配給內核。也就是說,應用程序只能使用剩下的 2GB 或 3GB 的地址空間,稱為用戶空間(User Space)。
Linux下32位環境的用戶空間內存分布情況
我們暫時不關心內核空間的內存分布情況,下圖是Linux下32位環境的一種經典內存模型:
對各個內存分區的說明:
內存分區 | s說明 |
---|---|
程序代碼區(code) | 存放函數體的二進制代碼。一個C語言程序由多個函數構成,C語言程序的執行就是函數之間的相互調用。 |
常量區(constant) | 存放一般的常量、字符串常量等。這塊內存只有讀取權限,沒有寫入權限,因此它們的值在程序運行期間不能改變。 |
全局數據區(global data) | 存放全局變量、靜態變量等。這塊內存有讀寫權限,因此它們的值在程序運行期間可以任意改變。 |
堆區(heap) | 一般由程序員分配和釋放,若程序員不釋放,程序運行結束時由操作系統回收。malloc()、calloc()、free() 等函數操作的就是這塊內存,這也是本章要講解的重點。注意:這里所說的堆區與數據結構中的堆不是一個概念,堆區的分配方式倒是類似於鏈表。 |
動態鏈接庫 | 用於在程序運行期間加載和卸載動態鏈接庫。 |
棧區(stack) | 存放函數的參數值、局部變量的值等,其操作方式類似於數據結構中的棧。 |
在這些內存分區中(暫時不討論動態鏈接庫),程序代碼區用來保存指令,常量區、全局數據區、堆、棧都用來保存數據。對內存的研究,重點是對數據分區的研究。
程序代碼區、常量區、全局數據區在程序加載到內存后就分配好了,並且在程序運行期間一直存在,不能銷毀也不能增加(大小已被固定),只能等到程序運行結束后由操作系統收回,所以全局變量、字符串常量等在程序的任何地方都能訪問,因為它們的內存一直都在。
常量區和全局數據區有時也被合稱為靜態數據區,意思是這段內存專門用來保存數據,在程序運行期間一直存在。
函數被調用時,會將參數、局部變量、返回地址等與函數相關的信息壓入棧中,函數執行結束后,這些信息都將被銷毀。所以局部變量、參數只在當前函數中有效,不能傳遞到函數外部,因為它們的內存不在了。
常量區、全局數據區、棧上的內存由系統自動分配和釋放,不能由程序員控制。程序員唯一能控制的內存區域就是堆(Heap):它是一塊巨大的內存空間,常常占據整個虛擬空間的絕大部分,在這片空間中,程序可以申請一塊內存,並自由地使用(放入任何數據)。堆內存在程序主動釋放之前會一直存在,不隨函數的結束而失效。在函數內部產生的數據只要放到堆中,就可以在函數外部使用。
一個實例
為了加深對內存布局的理解,請大家看下面一段代碼:
#include <stdio.h>
char *str1 = "c.biancheng.net"; //字符串在常量區,str1在全局數據區
int n; //全局數據區
char* func(){
char *str = "C語言"; //字符串在常量區,str在棧區
return str;
}
int main(){
int a; //棧區
char *str2 = "01234"; //字符串在常量區,str2在棧區
char arr[20] = "56789"; //字符串和arr都在棧區
char *pstr = func(); //棧區
int b; //棧區
printf("str1: %#X\npstr: %#X\nstr2: %#X\n", str1, pstr, str2);
puts("--------------");
printf("&str1: %#X\n &n: %#X\n", &str1, &n);
puts("--------------");
printf(" &a: %#X\n arr: %#X\n &b: %#X\n", &a, arr, &b);
puts("--------------");
printf("n: %d\na :%d\nb: %d\n", n, a, b);
puts("--------------");
printf("%s\n", pstr);
return 0;
}
運行結果:
str1: 0X400710
pstr: 0X400720
str2: 0X400731
--------------
&str1: 0X601040
&n: 0X60104C
--------------
&a: 0X19D0728C
arr: 0X19D07270
&b: 0X19D0726C
--------------
n: 0
a: -858993460
b: -858993460
--------------
C語言
對代碼的說明:
-
全局變量的內存在編譯時就已經分配好了,它的默認初始值是 0(它所占用的每一個字節都是0值),局部變量的內存在函數調用時分配,它默認初始值是不確定的,由編譯器決定,一般是垃圾值,這在《用一個實例來深入剖析函數進棧出棧的過程》中會詳細講解。
-
函數 func() 中的局部字符串常量"C語言中文網"也被存儲到常量區,不會隨着 func() 的運行結束而銷毀,所以最后依然能夠輸出。
-
字符數組 arr[20] 在棧區分配內存,字符串"56789"就保存在這塊內存中,而不是在常量區,大家要注意區分。
Linux下64位環境的用戶空間內存分布情況
在64位環境下,虛擬地址空間大小為 256TB,Linux 將高 128TB 的空間分配給內核使用,而將低 128TB 的空間分配給用戶程序使用。如下圖所示:

《虛擬地址空間以及編譯模式》一節中講到,在64位環境下,虛擬地址雖然占用64位,但只有最低48位有效。這里需要補充的一點是,任何虛擬地址的48位至63位必須與47位一致。上圖中,用戶空間地址的47位是0,所以高16位也是0,換算成十六進制形式,最高的四個數都是0;內核空間地址的47位是1,所以高16位也是1,換算成十六進制形式,最高的四個數都是1。這樣中間的一部分地址正好空出來,也就是圖中的“未定義區域”,這部分內存無論如何也訪問不到。