存管理:棧與堆的解析)
任何一個(gè)程序正常運(yùn)行都需要內(nèi)存資源用來存放諸如變量、常量、函數(shù)代碼等等。這些不同的內(nèi)容所存儲的內(nèi)存區(qū)域是不同的且不同的區(qū)域有不同的特性。因此我們需要研究內(nèi)存布局逐個(gè)了解不同內(nèi)存區(qū)域的特性。每個(gè) C 語言進(jìn)程都擁有一片結(jié)構(gòu)相同的虛擬內(nèi)存不同的程序會占據(jù)不同區(qū)段的物理內(nèi)存但經(jīng)過系統(tǒng)的變換和映射它們的虛擬內(nèi)存的布局是完全一樣的。將其中一個(gè) C 語言進(jìn)程的虛擬內(nèi)存放大來看會發(fā)現(xiàn)其內(nèi)部包含區(qū)域棧stack堆heap數(shù)據(jù)段代碼段1.1 棧內(nèi)存棧是現(xiàn)代計(jì)算機(jī)程序極為重要的組成部分幾乎所有的函數(shù)調(diào)用都依賴于棧進(jìn)行實(shí)現(xiàn)。??臻g中包括函數(shù)返回地址記錄了函數(shù)執(zhí)行完成后退出函數(shù)調(diào)用時(shí)需要執(zhí)行的下一條指令命令行參數(shù) ./a.exe abc 123局部變量包括形參函數(shù)調(diào)用過程中使用的非靜態(tài)局部變量均默認(rèn)保存在棧上函數(shù)退出后這些變量也會被釋放而不可使用保存的寄存器環(huán)境變量等保存了函數(shù)調(diào)用前后需要保持不變的寄存器在程序運(yùn)行過程中每次執(zhí)行函數(shù)調(diào)用都會建立一個(gè)棧幀連續(xù)的過程調(diào)用會讓棧不斷朝著“棧頂”的方向進(jìn)行擴(kuò)展在函數(shù)調(diào)用完成后棧幀會被自動(dòng)釋放棧也會隨之縮減。在大多數(shù)體系結(jié)構(gòu)上棧的擴(kuò)展方向都是由高地址向低地址進(jìn)行擴(kuò)展。棧內(nèi)存有什么特點(diǎn)空間有限尤其在嵌入式環(huán)境下。因此不可以用來存儲尺寸太大的變量。Linux 棧內(nèi)存大小為 8M系統(tǒng)為函數(shù)分配棧內(nèi)存時(shí)遵循從上高地址往下低地址分配原則由系統(tǒng)規(guī)定的我們無法干預(yù)每當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)系統(tǒng)將自動(dòng)分配一段棧內(nèi)存給該函數(shù)用于存放其局部變量每當(dāng)一個(gè)函數(shù)有退出時(shí)系統(tǒng)將自動(dòng)回收其棧內(nèi)存??臻g申請的變量隨著函數(shù)結(jié)束空間自動(dòng)釋放在函數(shù)的作用域中定義局部變量是保存在棧中在函數(shù)結(jié)束時(shí)系統(tǒng)會自動(dòng)回收此函數(shù)中創(chuàng)建的??臻g即 return 之后不可再訪問函數(shù)的局部變量因此返回一個(gè)局部變量的地址通常是錯(cuò)誤的。# include stdio.h int *add(int a, int b) { int sum; sum a b; return sum; //不能返回函數(shù)的局部變量地址 } int main(void) { int *p add(2,5); }1.2 堆內(nèi)存堆內(nèi)存heap又被稱為動(dòng)態(tài)內(nèi)存、自由內(nèi)存簡稱堆。堆是唯一可被開發(fā)者自定義的區(qū)段開發(fā)者可以根據(jù)需要申請內(nèi)存的大小、決定使用的時(shí)間長短等。但又由于這是一塊系統(tǒng)“飛地”所有的細(xì)節(jié)均由開發(fā)者自己把握系統(tǒng)不對此做任何干預(yù)給予開發(fā)者絕對的“自由”但也正因如此對開發(fā)者的內(nèi)存管理提出了很高的要求。對堆內(nèi)存的合理使用幾乎是軟件開發(fā)中的一個(gè)永恒的話題。例如 malloc 它分配的內(nèi)存來自進(jìn)程的虛擬內(nèi)存空間。虛擬內(nèi)存是操作系統(tǒng)提供的一種內(nèi)存管理機(jī)制它允許程序使用比物理內(nèi)存更多的內(nèi)存堆內(nèi)存基本特征相比棧內(nèi)存堆的總大小僅受限于物理內(nèi)存在物理內(nèi)存允許的范圍內(nèi)系統(tǒng)對堆內(nèi)存的申請不做限制。相比棧內(nèi)存堆內(nèi)存從下往上增長。堆內(nèi)存是匿名的只能由指針來訪問。自定義分配的堆內(nèi)存除非開發(fā)者主動(dòng)釋放否則永不釋放直到程序退出。堆內(nèi)存使用完成后一定要進(jìn)行手動(dòng)釋放內(nèi)存否則可能會使內(nèi)存泄漏導(dǎo)致程序崩潰相關(guān)API申請堆內(nèi)存malloc() / calloc()功能申請堆內(nèi)存所需頭文件#include stdlib.h函數(shù)原型void *malloc(size_t size);void *calloc(size_t count, size_tsize);參數(shù)size所申請的一塊堆內(nèi)存的大小單位是字節(jié)count所申請的堆內(nèi)存的塊數(shù)所有的內(nèi)存塊是連續(xù)分布的、無間隔的返回值成功返回所申請的內(nèi)存空間的指針失敗NULL#include stdio.h #include stdlib.h int main() { // malloc: 申請一塊內(nèi)存內(nèi)容是隨機(jī)的 int *p1 (int *)malloc(5 * sizeof(int)); // calloc: 申請 5 塊內(nèi)存申請的內(nèi)存會自動(dòng)清零 int *p2 (int *)calloc(5, sizeof(int)); free(p1); free(p2); return 0; }清零堆內(nèi)存bzero()功能清零指定內(nèi)存所需頭文件#include strings.h函數(shù)原型void bzero(void *s, size_t n);參數(shù)s指向要清零的內(nèi)存的指針n要清零的內(nèi)存大小單位是字節(jié)返回值無#include stdio.h #include stdlib.h #include strings.h int main() { int arr[5] {1, 2, 3, 4, 5}; bzero(arr, sizeof(arr)); // 整塊清零 printf(bzero后 arr[0] %d, arr[4] %d\n, arr[0], arr[4]); return 0; }釋放堆內(nèi)存free()功能釋放堆內(nèi)存所需頭文件#include stdlib.h函數(shù)原型voidfree(void *ptr);參數(shù)ptr堆內(nèi)存指針返回值無#include stdio.h #include stdlib.h int main() { int *p (int *)malloc(3 * sizeof(int)); p[0] 10; p[1] 20; p[2] 30; printf(使用前: p[1] %d\n, p[1]); free(p); // 釋放堆內(nèi)存 p NULL; // 置空防止懸空指針 printf(已釋放p NULL\n); return 0; }realloc()申請的堆內(nèi)存,在原來內(nèi)存基礎(chǔ)上可進(jìn)行擴(kuò)容。// 申請堆空間 char *p calloc(1, sizeof(char)); if (p NULL) { printf(calloc failed:); return -1; } char buf[] afjoiajfajflajlfjalflafjslfjsljfls; // 擴(kuò)容如果不擴(kuò)容會溢出 char *ptr realloc(p, 100); // 是將空間大小從 1 擴(kuò)容到 100不是擴(kuò)容了 100注意malloc()申請的堆內(nèi)存默認(rèn)情況下是隨機(jī)值一般需要用 bzero() 來清零。calloc()申請的堆內(nèi)存默認(rèn)情況下是已經(jīng)清零了的不需要再清零。free()只能釋放堆內(nèi)存不能釋放別的區(qū)段的內(nèi)存。釋放內(nèi)存的含義釋放內(nèi)存意味著將內(nèi)存的使用權(quán)歸還給系統(tǒng)。釋放內(nèi)存并不會改變指針的指向。釋放內(nèi)存并不會對內(nèi)存做任何修改更不會將內(nèi)存清零。內(nèi)存泄漏示例以下是一個(gè)典型的內(nèi)存泄漏代碼在函數(shù)中申請堆內(nèi)存后忘記釋放導(dǎo)致每次調(diào)用都泄漏內(nèi)存。#include stdio.h #include stdlib.h void leak_memory() { int *p (int *)malloc(100 * sizeof(int)); // 忘記調(diào)用 free(p)函數(shù)返回后指針 p 丟失內(nèi)存卻未歸還系統(tǒng) } int main() { for (int i 0; i 100000; i) { leak_memory(); // 每次調(diào)用泄漏 400 字節(jié)累計(jì)約 40MB } printf(程序結(jié)束\n); return 0; }后果隨著循環(huán)不斷調(diào)用leak_memory()堆內(nèi)存持續(xù)增長而永不釋放。在長時(shí)間運(yùn)行的程序如服務(wù)器進(jìn)程中內(nèi)存泄漏會逐漸耗盡可用內(nèi)存最終導(dǎo)致系統(tǒng) OOMOut of Memory或程序崩潰。即使程序結(jié)束泄漏的內(nèi)存在程序運(yùn)行期間也無法被其他進(jìn)程使用。如何避免配對使用 malloc 和 free每次調(diào)用malloc后確保在合適的時(shí)機(jī)調(diào)用free。使用內(nèi)存檢測工具如 ValgrindLinux或 Dr. MemoryWindows檢測內(nèi)存泄漏。遵循誰申請誰釋放原則明確內(nèi)存的所有權(quán)和釋放責(zé)任避免遺漏。在函數(shù)退出前統(tǒng)一釋放用goto或if分支確保所有路徑都能釋放已申請的內(nèi)存。1.3 堆與棧的區(qū)別1、 管理方式不同:棧由系統(tǒng)自動(dòng)分配空間同時(shí)系統(tǒng)自動(dòng)釋放空間。堆需要程序員手動(dòng)申請并且手動(dòng)釋放并指明大小。在 C 語言中 malloc 函數(shù)申請釋放 free 函數(shù)在 C 中 new 和 delete 實(shí)現(xiàn)。2、 存儲內(nèi)容不同:棧在函數(shù)調(diào)用時(shí)棧中存放的是函數(shù)中各個(gè)參數(shù)(局部變量)堆一般是在堆的頭部用一個(gè)字節(jié)存放堆的大小不是必須的。堆中的具體內(nèi)容由程序員安排。3、 空間大小不同:棧獲取空間較小。堆比較靈活比較大。4、 能否產(chǎn)生碎片不同:棧不會產(chǎn)生碎片空間連續(xù)。堆采用的是鏈表的存儲方式會產(chǎn)生碎片。5、 分配方式不同:棧靜態(tài)分配由編譯器完成堆動(dòng)態(tài)分配的使用malloc函數(shù)6、 分配效率不同棧由系統(tǒng)自動(dòng)分配速度較快。但程序員是無法控制的。堆由malloc分配的內(nèi)存一般速度比較慢而且容易產(chǎn)生內(nèi)存碎片不過用起來方便。7、 生長方向不同棧向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)是一塊連續(xù)的內(nèi)存的區(qū)域。堆向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)是用鏈表來存儲的空閑內(nèi)存地址的自然是不連續(xù)的而鏈表的遍歷方向是由低地址向高地址。