相對于其他語言,C、C++的一大利器便是可以非常靈活的控制記憶體。與此同時,另一方面靈活的帶來的要求也是十分嚴格,否則會出現令人頭疼的配置設定錯誤、記憶體越界、記憶體洩漏等衆多記憶體問題。
C程式的記憶體結構分為兩種,一種是存儲在磁盤時的結構,一種是程式運作時的結構。兩者的差別在與運作時,系統會為其多配置設定堆棧空間。

圖:C程式記憶體結構
下面通過一個例子看看具體的配置設定
輸出的結果。
堆與棧
棧是一種的“先進後出”的存儲結構。
堆是一種完全二叉樹。節點從左到右填滿,最後一層的樹葉都在最左邊。(即如果一個節點沒有左邊兒子,那麼它一定沒有右邊兒子),每個節點的值都小于(或者大于)其子節點的值(大頂堆、小頂堆)。它的特點是可以使用一維數組來表示。堆的操作也可通過資料元素交換的形式解決,非常适合記憶體空間線性的特點。
C語言中,記憶體配置設定有三種
靜态區域配置設定:由編譯器自動配置設定與釋放,記憶體在編譯的時候已經配置設定好,這塊記憶體在整個程式的運作期間都存在直到程式結束時才被釋放,如全局變量與static變量。
棧配置設定:由編譯器在程式運作時從棧上配置設定,函數棧退出時自動釋放。棧配置設定的運算在處理器的指令集中,是以它的運作效率很高,但能配置設定的内容是有限的。
堆配置設定:有程式員主動調用記憶體配置設定函數來申請記憶體,且使用完畢後由程式員自己釋放,其使用非常靈活,但其配置設定方式是通過調用函數來實作,效率沒棧高。malloc,alloc等
先從彙編的角度來看堆與棧在配置設定空間的差別。
下面這個例子非常簡單,給棧變量a指派0xA,給堆配置設定的變量指派0xB
下面是彙編代碼
對于棧變量a的記憶體配置設定與指派,這裡的做法非常簡單,主要的兩個指令便可以完成配置設定與指派操作。
1.棧頂指針寄存器向低位址移動0x30個位元組空間(subq $0x30, %rsp),也可以了解為配置設定了0x30個位元組空間給目前堆棧,而此時棧中已經包含變量a的空間。
2.将直接将立即數0xA指派給變量a movl $0xa, -0x14(%rbp)。此時我們可以知道,相對于棧基指針寄存器向低位址偏移0x14的位址便是a的記憶體區域。
3.當函數執行完畢後,棧頂指針寄存器rsp與棧基位址寄存器rbp,回退到上一函數,步驟1中配置設定的的空間也同時被釋放。
而堆變量b的記憶體配置設定與指派,則可以看到其是通過調用callq 0x100000f68實作的(此處0x100000f68指的是malloc函數的位址)。也就是變量b記憶體的配置設定是由malloc函數内部實作,并沒有像棧變量配置設定一樣通過簡單的兩個指令便可以完成。
從堆中配置設定記憶體,配置設定大小為size。若配置設定成功,傳回記憶體首位址,如果配置設定失敗,傳回NULL。
從堆中配置設定記憶體,配置設定count個相鄰的記憶體單元,每個單元大小為size。若配置設定成功,傳回記憶體首位址,如果配置設定失敗,傳回NULL。
從功能上看,該函數與malloc差不不大,不同的是calloc函數會将記憶體初始化為0。
有人會問既然calloc已經覆寫malloc所做的事情,而且還非常友善的将記憶體初始化為0,那malloc不就不太有用了嗎?其實在調用方看來malloc而不需要初始化為0的情況,可能配置設定記憶體後馬上指派了有用的資料,不需要初始化為0.
用于更改已經配置的記憶體空間,其同樣是從堆中配置設定記憶體。
當程式需要擴大空間時,函數試圖從堆上目前記憶體段後的位元組中擷取更多的記憶體空間,如有足夠的存儲空間,則擴大記憶體後傳回原位址。如果目前記憶體段後的位元組不夠,則使用記憶體堆上滿足要求的其他記憶體塊,并将原有的資料拷貝至新配置設定的區域,然後釋放原有區域,傳回新區域的指針。
不同于malloc、calloc、realloc是從堆中配置設定記憶體,alloca是從棧中配置設定空間。正因其從棧中配置設定的記憶體,是以無需手動釋放記憶體。
使用malloc、calloc、realloc從堆中配置設定記憶體時,需要及時釋放
使用記憶體配置設定函數擷取指針變量時,需堆配置設定函數的傳回值進行判空處理。
因記憶體配置設定函數可能會因為其他的一些不可預知的情況導緻配置設定失敗。
因記憶體配置設定函數傳回值都為void (也稱無類型),而且void 無法對該一段記憶體區域進行移位通路操作,是以在使用配置設定函數必須對其轉換成其他類型,以便進行操作。
在使用malloc進行配置設定時,因該記憶體函數為進行初始化,若此時對記憶體進行通路,很可能會造成程式崩潰
C99規定,程式嘗試配置設定長度為0的記憶體時,該行為是由具體編譯器所決定的。可能會到時程式崩潰,可能傳回一個NULL指針。是以需要避免此行為。
一塊記憶體區域使用free釋放後,需要養成将其設定為NULL的習慣,以避免在程式錯誤的再次通路指針時造成野指針通路錯誤。
對于不是通過記憶體配置設定函數擷取的空間,禁止非法通路。
C語言中,隻要是一個指針變量,那就需要確定其指向是一段合法有效的值。
如上例子中,需要給指針變量配置設定一段合法的記憶體
在釋放c語言中的結構體時,需要確定其成員屬性中的所有記憶體都釋放,以免出現記憶體洩漏。
延續上面的例子
僅僅釋放std指針是不夠的,需要釋放其name成員,而且釋放的順序也需要注意,是先成員後對象。
各個記憶體配置設定函數中對于大小的參數都是size_t,在配置設定記憶體時需要確定避免申請過大的記憶體空間。
C語言中對于記憶體使用是十分靈活與友善的,正是由于其過于靈活,我們在使用它時,需要對于配置設定出來的記憶體塊的大小,初始化,生命周期,釋放時機,釋放方法,有個非常清楚的了解,要清楚的了解配置設定的每一塊記憶體的去向,做到胸有成竹才能用好這一利器。