最近花了點(diǎn)時(shí)間讀了《深入理解C指針》這本書,讀完這本書后,對(duì)于之前嵌入式C語言開發(fā)中很多一知半解的地方豁然開朗。對(duì)于之前學(xué)習(xí)以及工作中,很多沒有注意的問題,也有了更加深刻的理解和認(rèn)識(shí)。今天就花點(diǎn)時(shí)間整理下這段時(shí)間讀完這本書后的所學(xué)所得,也方便后續(xù)查看。
我們?cè)陂_發(fā)C語言程序的時(shí)候,程序需要在編譯器中編譯后,才能在對(duì)應(yīng)產(chǎn)品中運(yùn)行。在程序運(yùn)行的過程中,內(nèi)存的分配一般分為以下幾個(gè)部分:
- 堆(heap)
- 棧(stack)
- BSS區(qū)
- 數(shù)據(jù)區(qū)
- 代碼區(qū)
對(duì)于我們平時(shí)開發(fā)的C程序,加載到處理器的內(nèi)存中運(yùn)行,呈現(xiàn)出來的結(jié)構(gòu)就大概分為以上五個(gè)區(qū)域。為了更好地理解,我們可以根據(jù)下面這張圖去理解。

下面我們簡單地對(duì)上述幾個(gè)區(qū)域做一下介紹。首先從代碼區(qū)開始。
代碼區(qū)
我們需要了解的是,我們?cè)谑褂肅語言開發(fā)產(chǎn)品程序代碼的時(shí)候,最終的開發(fā)好的程序是并不能直接在產(chǎn)品的處理器中直接運(yùn)行的。而是需要經(jīng)過編譯器的編譯,最終將我們的代碼轉(zhuǎn)換成CPU處理器所能夠理解的機(jī)器指令,才能在CPU中正常運(yùn)行。而代碼區(qū),我們可以簡單地理解為我們編寫好的程序(已經(jīng)轉(zhuǎn)換為機(jī)器指令的程序代碼)存放的地方。CPU處理器會(huì)從這個(gè)地方取出機(jī)器指令去完成對(duì)應(yīng)的操作,最終實(shí)現(xiàn)我們想讓CPU所實(shí)現(xiàn)的功能(簡單的算術(shù)運(yùn)算,或者對(duì)應(yīng)的硬件操作等)。
棧區(qū)
棧區(qū),這個(gè)區(qū)域不需要我們?nèi)ゾS護(hù)管理,在程序運(yùn)行的過程中,會(huì)自動(dòng)的分配和釋放??赡苡行┤瞬惶芾斫膺@個(gè)區(qū)域是做什么用的,這里我們簡單地做一下介紹。
我們?cè)陂_發(fā)嵌入式C程序代碼的過程中,經(jīng)常會(huì)在定義一些函數(shù),去完成特定的功能(例如:定義一個(gè)addfunc函數(shù)用于實(shí)現(xiàn)兩個(gè)數(shù)的加法運(yùn)算,或者說更加復(fù)雜的,定義一個(gè)函數(shù)來對(duì)一個(gè)鏈表進(jìn)行查詢操作),在這些函數(shù)中,我們不可避免地,會(huì)去定義一些局部變量(局部變量的概念就不在這里做介紹了,不理解的建議去過一遍譚浩強(qiáng)的C語言程序開發(fā))。在程序運(yùn)行的過程中,我們需要一個(gè)地方去存儲(chǔ)我們定義的這些局部變量,調(diào)用函數(shù)時(shí)的參數(shù)值,返回值等,而這個(gè)地方就是棧。
棧區(qū)是處于不斷變化的狀態(tài)中,舉個(gè)例子,當(dāng)前,程序運(yùn)行在main函數(shù)中,此時(shí)棧中存儲(chǔ)的則是main函數(shù)中我們定義的一些局部變量。這個(gè)時(shí)候,假如我們?cè)趍ain函數(shù)中去調(diào)用其他函數(shù),那么,棧中會(huì)自動(dòng)進(jìn)行出棧操作,然后再進(jìn)行入棧操作,將我們調(diào)用的那個(gè)函數(shù)的相關(guān)的局部變量,函數(shù)參數(shù),返回值等都?jí)喝氲綏V?。如下圖所示:

最先開始,程序從main函數(shù)開始運(yùn)行,在調(diào)用addfunc函數(shù)之前,棧中的存儲(chǔ)內(nèi)容可能是這樣的。


而在調(diào)用了addfunc函數(shù)后,棧區(qū)的內(nèi)容就會(huì)被自動(dòng)地替換為addfunc函數(shù)中的局部變量等內(nèi)容。在返回main函數(shù)后,又會(huì)自動(dòng)進(jìn)行出棧入棧操作。

我們?cè)诓粩嗟卣{(diào)用函數(shù)的同時(shí),也自動(dòng)不斷地進(jìn)行入棧以及出棧的操作,也就是說,我們定義的局部變量,實(shí)際上是在內(nèi)存上不是一直會(huì)存在的,可能會(huì)在某一次的入棧及出棧操作中被覆蓋掉。舉個(gè)例子,我們可以試著編譯運(yùn)行下面這段代碼。

這里我們?cè)趍ain函數(shù)中定義了一個(gè)局部變量a和b,以及一個(gè)res變量。接著我們調(diào)用addfunc函數(shù),先將a和b變量的值進(jìn)行相加,然后存儲(chǔ)到res變量中。最后輸出結(jié)果??吹竭@里,如果對(duì)C語言有點(diǎn)基礎(chǔ)的朋友,就知道這種做法是得不到我們想要的預(yù)期結(jié)果的。而這就是因?yàn)槲覀兂绦蛟谶\(yùn)行過程中,棧區(qū)不斷進(jìn)行入棧出棧導(dǎo)致的。我們?cè)趍ain函數(shù)中定義了a,b和res后,調(diào)用addfunc函數(shù),此時(shí)棧區(qū)會(huì)進(jìn)行出棧以及入棧操作,在addfunc函數(shù)中,做完加法運(yùn)算后,res變量的值存儲(chǔ)在棧中某個(gè)地址中,但是在addfunc函數(shù)返回后,會(huì)被下一次出棧入棧操作給覆蓋掉。因此我們得不到我們想要的結(jié)果。
堆區(qū)
上面介紹完棧區(qū)后,我們這里再簡單說下堆區(qū)。其實(shí)堆區(qū)和棧區(qū),指向的是同一片內(nèi)存區(qū)域。不同的是,這兩個(gè)區(qū)域在這一塊內(nèi)存區(qū)域的兩頭,隨著程序的運(yùn)行,根據(jù)需要,一個(gè)不斷向下增長,一個(gè)不斷向上增長。(如下圖)這也是我們經(jīng)常喜歡說“堆棧“,而不是將這兩個(gè)地方單獨(dú)說”棧區(qū)“或”堆區(qū)“的原因。

堆區(qū)不同于棧區(qū)的另一個(gè)地方,就是我們程序想要在堆上開辟內(nèi)存使用,方法不同于棧。在C程序中,我們定義了一個(gè)局部變量并賦值,這個(gè)過程可以理解為我們?cè)跅I祥_辟了一個(gè)空間來使用,而且我們不需要去理會(huì)這個(gè)空間后續(xù)的回收問題。但是在堆上開辟空間的方法則不同。我們可以調(diào)用malloc函數(shù)在堆上開辟我們所需要使用的內(nèi)存空間。在最后這段空間使用完畢后,則需要我們?cè)僬{(diào)用free函數(shù)來釋放掉我們申請(qǐng)的這段空間。否則就會(huì)發(fā)生內(nèi)存泄露問題。當(dāng)內(nèi)存泄露的次數(shù)多了,則可能出現(xiàn)堆棧溢出。簡單的說,就是我們一直向系統(tǒng)索要內(nèi)存空間來使用,卻忘記了“歸還“,系統(tǒng)內(nèi)存空間是有限的,當(dāng)系統(tǒng)內(nèi)存空間不夠程序運(yùn)行使用時(shí),則會(huì)導(dǎo)致堆棧溢出,程序再也無法正常運(yùn)行。
BSS區(qū)
這個(gè)區(qū)域存放的是未初始化的全局變量和靜態(tài)變量。
數(shù)據(jù)區(qū)
這個(gè)區(qū)域存放的是已初始化的全局變量和靜態(tài)變量,以及常量等。如下圖:

我們?cè)趍ain函數(shù)中,定義了a,b,c三個(gè)變量,a和b變量均保存去數(shù)據(jù)區(qū)中,而c是一個(gè)指針,是局部變量,存放的是“hello world”字符串的地址,而這個(gè)字符串則存在于數(shù)據(jù)區(qū)。如下圖
