前言
C語言作為一門應用途廣泛、功能強大、使用靈活的面向過程式編程語言。既可用于編寫應用軟件,又能用于編寫系統軟件。所以深入理解C語言的內存管理能夠加深我們對程序的理解,有助于開發出更高質量的應用。本文就是筆者在學習C語言過程中對其內存管理的學習總結,分享出來希望能給正在學習C、Objective-C、C++等語言的小伙伴們一點啟發。
內存定義
- 硬件角度:內存是計算機必不可少的一個組成部分,是于CPU溝通的橋梁,計算機中所有的程序都是運行在內存中的。
- 邏輯角度:內存是一塊具備隨機訪問能力,支持讀、寫操作,用來存放程序及程序運行中產生的數據的區域。
- 聯想理解:以現實生活中的建筑物來類比最合適不過,計算機中可用的內存對應著一棟大樓,內存中的每一個單元格的地址對應著房間的門牌號。內存中存儲的內容就是住在房間里面的人或物。我們根據內存單元格的地址就能找到對應內存中存儲的數據。
內存單位和編址
- 位 :( bit ) 是電子計算機中最小的數據單位。每一位的狀態只能是0或1。
- 字節:1 Byte = 8 bit ,是內存基本的計量單位,
- 字:"字" 由若干個字節構成,字的位數叫做字長,不同檔次的機器有不同的字長。
- KB :1KB = 1024 Byte。也就是1024個字節。
- MB : 1MB = 1024 KB。類似的還有GB、TB。
- 內存編址:計算機中的內存按字節編址,每個地址的存儲單元可以存放一個字節(8個bit)的數據,CPU通過內存地址獲取指令和數據,并不關心這個地址所代表的空間具體在什么位置、怎么分布,因為硬件的設計保證一個地址對應著一個固定的空間,所以說:內存地址和地址指向的空間共同構成了一個內存單元。
-
內存地址:內存地址通常用十六進制的數據表示,例如通常在C或者Objective-C中輸出一個變量的地址可能為:0x7fff5fbff79c,這就是一個用十六進制的數表示的地址。
某塊內存的存儲單元分配可能如下:
內存單元分配示意圖
由上面的圖可以看出:內存是由字節為單位組成的,字節在內存中是連續分配的,一個字節單元的地址一般由十六進制的數表示。 - 十六進制:計算機中數據的一種表示方法。由0-9,A-F組成,字母不區分大小寫。C、C++規定,16進制數必須以 0x 開頭。比如 0x1 表示一個16進制數。而 1 則通常表示一個十進制數。另外如:0xff、0xFF、0X102A等等。其中的x也不區分大小寫。(注意:0x 中的0是數字0,而不是字母O)。使用十六進制表示一個內存地址是因為:1,二進制、十進制、十六進制之間相互轉換比較方便;2,一位十六進制數可以表示4個二進制位數,更大的數使用十六進制數表示更加精短。3,計算機硬件設計需要。例如下圖的整數100在三種進制中的表示:
二進制、十進制、十六進制表示
內存組成
對于一個由C語言編寫的程序而言,內存主要可以分為以下5個部分組成:
其中需要注意的是:代碼段、數據段、BSS段在程序編譯期間由編譯器分配空間,在程序啟動時加載,由于未初始化的全局變量存放在BSS段,已初始化的全局變量存放在數據段,所以程序中應該盡量少的使用全局變量以節省程序編譯和啟動時間;棧和堆在程序運行中由系統分配空間。下面簡單介紹一下各個組成部分具體含義,重點介紹堆、棧。
-
棧(stack)
首先棧應該作為數據結構中的一種線性結構被介紹。其具有先進后出的特點(簡稱FILO)。
棧的結構示意圖
類似于手槍中的彈夾(別告訴我你沒打過手槍...),最先放入的子彈最后彈出,最后放入的子彈最先彈出,通過不斷移動棧頂指針實現子彈的裝載和發射(對應棧的操作是入棧和出棧)。與棧對應的還有一種數據結構叫做 “隊列”,具有先進先出(FIFO)的特點,這里就不再贅述了,有興趣的同學可以看看關于“數據結構”方面的書了解更多。
其次棧作為內存中存儲結構,通常存放程序臨時創建的局部變量,即函數括大括號 “{ }” 中定義的變量,其中還包括函數調用時其形參,調用后的返回值等。 棧是由到高地址向低地址擴展的數據結構。即依次定義兩個局部變量,首先定義的變量的地址是高地址,其次變量的地址是低地址。例如
#include <stdio.h>
int main(int argc, const char * argv[]) {
int a = 100;
int b = 100;
printf("%p \n",&a); // 0x7fff5fbff79c
printf("%p \n",&b); // 0x7fff5fbff798
return 0;
}
// a 變量的地址 0x7fff5fbff79c 比 b 變量的地址 0x7fff5fbff798 要大
最后棧還具有“小內存、自動化、可能會溢出”的特點。棧頂的地址和棧的最大容量一般是系統預先規定好的,通常不會太大。由于棧中主要存放的是局部變量,而局部變量的占用的內存空間是其所在的代碼段或函數段結束時由系統回收重新利用,所以棧的空間是循環利用自動管理的,一般不需要人為操作。如果某次局部變量申請的空間超過棧的剩余空間時就有可能出現 “棧的溢出”,進而導致意想不到的后果。所以一般不宜在棧中申請過大的空間,比如長度很大的數組、遞歸調用重復次數很多的函數等等。
-
堆(heap)
通常存放程序運行中動態分配的存儲空間。堆是低地址向高地址擴展的數據結構,是一塊不連續的內存區域。在標準C語言上,使用malloc等內存分配函數是從堆中分配內存的,在Objective-C中,使用new創建的對象也是從堆中分配內存的。
堆具有“大內存、手工分配管理、申請大小隨意、可能會泄露”的特點,堆內存是操作系統劃分給堆管理器來管理的,管理器向使用者(用戶進程)提供API(malloc和free等)來使用堆內存。需要程序員手動分配釋放,如果程序員在使用完申請后的堆內存卻沒有及時把它釋放掉,那么這塊內存就丟失了(進程自身認為該內存沒被使用,但是在堆內存記錄中該內存仍然屬于這個進程,所以當需要分配空間時又會重新去申請新的內存而不是重復利用這塊內存),就是我們常說的-內存泄漏,所以內存泄漏指的是堆內存被泄露了。下面是一個很典型關于各種變量在內存中分配位置的例子(示例代碼來源于網絡):
#include <stdio.h>
int a = 0; // 全局初始化區
char p1; // 全局未初始化區
int main(int argc, const char * argv[]) {
int b ; // 棧
char s[] = "abc"; // 棧
char p2 ; // 棧
char p3 = "123456"; // 123456在常量區,p3在棧上。
static int c = 0 ; // 全局(靜態)初始化區
p1 = (char )malloc(10); // 分配的10字節的區域就在堆區
p2 = (char )malloc(20); // 分配的20字節的區域就在堆區
printf("%p\n",p1); // 0xffffffb0
printf("%p\n",p2); // 0xffffffc0
return 0;
//p1 變量的地址 0xffffffb0 比 p2 變量的地址 0xffffffc0 要小
}
-
BSS段
Block Started by Symbol的簡稱,通常是指用來存放程序中未初始化的全局變量和靜態變量。 -
數據段
通常是指用來存放程序中已初始化的全局變量和靜態變量以及字符串常量 -
代碼段
通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定。
C語言操作堆內存的函數
C語言中申請和釋放堆內存有四個函數,分別是:
-
malloc
【函數原型】void *malloc(size_t __size)
【參數說明】size
需要分配的內存空間的大小,單位是字節。
【返回值類型】void *
表示未確定類型的指針,分配成功返回指向該內存的地址,失敗則返回NULL
。C、C++規定,void *
類型可以強制轉換為任何其它類型的指針。
【函數功能】 表示向系統申請分配指定 size 個字節的內存空間。例如:
int *a = malloc(4); //申請4個字節的空間用于存放一個int類型的值
char *b = malloc(2); //申請2個字節的空間用于存放一個char類型的值
-
calloc
【函數原型】void *calloc(size_t __count, size_t __size)
【參數說明】count
表示個數,size
單位個需要分配的內存空間的大小,單位是字節。
【返回值類型】void *
表示未確定類型的指針。例如:
【函數功能】 表示向系統申請分配 count 個長度為 size 一共為 count 乘以 size 個字節長度的連續內存空間,并將每一個字節都初始化為 0。
int *c = calloc(10, sizeof(int)); 申請10個sizeof(int) 字節的空間
char *d = calloc(2, sizeof(char)); 申請10個sizeof(char) 字節的空間
-
realloc
【函數原型】void *realloc(void *__ptr, size_t __size)
【參數說明】ptr
表示需要修改的內存空間的地址,size
表示需要重寫分配的內存空間的大小,單位是字節。
【返回值類型】void *
表示未確定類型的指針。
【函數功能】 表示更改已經配置好的內存空間到指定的大小。例如:
char *d = calloc(2, sizeof(char)); //申請2個sizeof(char) 字節的空間
char *f = realloc(d, 5 * sizeof(char)); //將原來變量d指向的2個sizeof(char) 字節的空間更改到5個sizeof(char) 字節的空間并由變量f指向。
-
free
【函數原型】void free(void *)
【參數說明】void *
表示需要釋放的內存空間對應的內存地址。
【返回值類型】返回值為空。
【函數功能】 表示用來釋放已經動態分配的內存空間。free()
可以釋放由malloc()
、calloc()
、realloc()
分配的內存空間,以便其他程序再次使用。需要注意的是:free()
不會改變 傳入的指針的值,調用free()
后它仍然會指向相同的內存空間,但是此時該內存已無效,不能被使用。所以建議將釋放完的指針設置為NULL
。例如:
char *g = malloc(sizeof(char)); //申請sizeof(char)大小內存空間
free(g); //釋放掉g指針指向的內存空間
g = NULL; //將g指針指向NULL
free()
函數只能釋放動態分配的內存,并不能釋放任意分配的內存,比如:
int h[10]; //在棧上申請的10乘以sizeof(int)大小的內存空間
free(h); //此處報錯:不能釋放棧上分配的空間
void 指針類型
在系統提供的內存分配的4個庫函數malloc
、calloc
、recolloc
、free
中,除了free
的返回值為空外,其他三個函數的返回值均為void *
類型,由于指針不是本文的重點,所以這里僅僅簡單介紹一下關于void
指針類型(即void *
類型)。
《C語言程序設計》一書中對void
指針類型是這樣解釋的:
C99(C語言的官方標準第二版)允許使用基類型為
void
的指針類型,可以定義一個基類型為void
的指針變量,它不指向任何類型的數據。請注意:不要把 “指向void
類型” 理解為能指向 “任何的類型” 的數據,而應該理解為 “指向空類型” 或者 “不指向確定” 的類型的數據。在將它的值賦給另一個指針變量時由系統對它進行類型轉換,使之適合被賦值變量的類型。
例如下面的代碼:
int main(int argc, const char * argv[]) {
int a = 3; //定義a為整型變量
int *p1 = &a; //p1指向 int 型變量
char *p2; //p2指向 char 型變量
void *p3; //p3為無類型指針變量
p3 = (void *)p1; //將p1的值轉換為void *類型,然后賦值給p3
p2 = (char *)p3; //將p3的值轉換為char *類型,然后賦值給p2
printf("%d\n", *p1); //輸出a的值 3
p3 = &a;
printf("%d", *p3); //此處報錯,p3無指向,不能指向a
return 0;
}
C99標準把上述三個函數的基類型定義為void
類型,這種指針稱之為無類型指針,即不指向哪一種具體的類型數據,只表示用來指向一個抽象類型的數據,僅僅提供一個純地址,不能指向任何具體的對象。
變量的存儲方式
-
變量的作用域
局部變量:在函數內定義的變量,只在該函數內有效
全局變量:在函數外定義的變量,從定義開始到文件結束都有效
全局變量的作用范圍
resultA
、resultB
都是全局變量,但是作用范圍不同,在add()
、sub()
、main()
函數中都可以使用resultA
,但是add()
不能使用resultB
變量。
-
變量的存儲規律
變量在內存中以二進制形式存儲,一個變量占用的存儲空間,不僅和變量類型有關,還和編譯環境有關,同一種類型的變量在不同編譯環境下占用的存儲空間不一樣。比如開發中常用的基本數據類型char
、int
等在不同編譯環境下就會占用不同大小的空間。
占用空間和編譯環境示意圖類似下面的代碼在64位編譯器中其內存中地址和分配的內存空間應該如下圖:
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
printf("%p\n",&a); //0x7fff5fbff79b
printf("%p\n",&b); //0x7fff5fbff797
return 0;
}
局部變量存儲細節:由于是a、b是臨時變量,因此他們的內存空間分配在棧上,棧中內存尋址由高到低,所以 a 變量的地址比 b 變量的地址要大,其次由于是在64位編譯環境中,int 型變量占據4個字節的空間,每一個字節由低到高依次對應著8位二進制數,四個8位二進制數就是十進制中的 1 或 2,而變量a、b的地址就是四個字節中最小值的內存地址。
全局變量存儲細節:關于全局變量存儲在前面介紹內存組成已經說明,這里不再贅述。
變量的存儲類別:C的存儲類別包括4種:auto(自動的)、static(靜態的)、register(寄存器的)、extern(外部的)。根據變量的存儲類別可以得知其作用域和生命周期。
數組的存儲
C語言中數組的存儲和普通的變量不太一樣,數值中存儲的元素,是從所占用的低地址開始存儲的。例如:
int main(int argc, const char * argv[]) {
char chars[4] = {'l','o','v','e'};
printf("chars[0] = %p\n",&chars[0]); //0x7fff5fbff79b
printf("chars[1] = %p\n",&chars[1]); //0x7fff5fbff79c
printf("chars[2] = %p\n",&chars[2]); //0x7fff5fbff79d
printf("chars[3] = %p\n",&chars[3]); //0x7fff5fbff79e
return 0;
}
僅僅通過上面的字符數組例子還不能完全說明數組在內存中關于其元素存儲和元素中值的存儲關系。如果換用一個整型數組就能看出一些差別。
int main(int argc, const char * argv[]) {
int nums[2] = {5, 6};
printf("nums[0] = %p\n",&nums[0]); // 0x7fff5fbff7a0
printf("nums[1] = %p\n",&nums[1]); // 0x7fff5fbff7a4
return 0;
}
通過字符數組和整型數組內存分配示意圖可以看出:數組中的元素按照存放順序依次從低地址到高地址存放,但是每個元素中的內容又是按高地址向低地址方向存儲。
數組在使用過程中遇到的最多的問題可能就是下標越界,下面的代碼就是越界訪問數組示例:
int main(int argc, const char * argv[]) {
char charsOne[2] = {'a', 'b'};
char charsTwo[3] = {'c', 'd', 'e'};
charsTwo[3] = 'f';
printf("charsOne[0] = %p\n",&charsOne[0]); // 0x7fff5fbff79e
printf("charsTwo[0] = %p\n",&charsTwo[0]); // 0x7fff5fbff79b
printf("charsOne[0] = %c\n",charsOne[0]); // f
return 0;
}
結合下標越界示意圖看上面的的代碼會發現,由于越界設置
charsTwo[3]
元素的值,導致變相更改了charsOne[0]
的值。
int main(int argc, const char * argv[]) {
char charsOne[2] = {'a', 'b'};
char charsTwo[3] = {'c', 'd', 'e'};
charsOne[-1] = 'g';
printf("charsTwo[2] = %c\n",charsTwo[2]); // g
return 0;
}
同樣的道理,由于越界訪問
charsOne[-1]
元素,但是charsOne
并不存在這個元素,編譯器會向上尋找到charsTwo [2]
的位置,導致變相更改了charsTwo[2]
的值。
通過數組在內存中的分配圖能夠更好幫助我們理解數組越界問題的嚴重性,在開發中使用數組首先應該考慮的就是數組長度問題,盡量不要出現類似低級錯誤。
總結
以上就是筆者對于C語言內存管理基本概念的學習心得,非常的基礎和入門,在接下來的文章中還可能會深入探討關于“一般函數調用”、“遞歸函數調用”、“結構體和指針”等在C語言中內存的分配。
如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來;喜歡我的文章,可以關注我以此促進交流學習; 如果覺得此文戳中了你的G點請隨手點贊;轉載請注明出處,謝謝支持。
下一篇:C語言-內存管理深入