1. 程式人生 > >很多人不瞭解的嵌入式C高階用法(一)

很多人不瞭解的嵌入式C高階用法(一)

本文來自網路

1、記憶體管理

我們需要知道——變數,其實是記憶體地址的一個抽像名字罷了。在靜態編譯的程式中,所有的變數名都會在編譯時被轉成記憶體地址。機器是不知道我們取的名字的,只知道地址。 

記憶體的使用時程式設計中需要考慮的重要因素之一,這不僅由於系統記憶體是有限的(尤其在嵌入式系統中),而且記憶體分配也會直接影響到程式的效率。因此,我們要對C語言中的記憶體管理,有個系統的瞭解。 

在C語言中,定義了4個記憶體區間:程式碼區;全域性變數和靜態變數區;區域性變數區即棧區;動態儲存區,即堆區;具體如下: 

1>棧區(stack)— 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。

 2>堆區(heap) — 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。 

3>全域性區(靜態區)(static)—全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的 另一塊區域。 - 程式結束後由系統釋放。 

4>常量區 —常量字串就是放在這裡的。 程式結束後由系統釋放。 

5>程式程式碼區—存放函式體的二進位制程式碼。   

我們來看張圖:  

圖1

首先我們要知道,原始碼編譯成程式,程式是放在硬碟上的,而非記憶體裡!只有執行時才會被呼叫到記憶體中!我們來看看程式結構,ELF是是Linux的主要可執行檔案格式。ELF檔案由4部分組成,分別是ELF頭(ELF header)、程式頭表(Program header table)、節(Section)和節頭表(Section header table)。具體如下: 

1>Program header描述的是一個段在檔案中的位置、大小以及它被放進記憶體後所在的位置和大小。即要載入的資訊; 

2>Sections儲存著object 檔案的資訊,從連線角度看:包括指令,資料,符號表,重定位資訊等等。在圖中,我們可以看到Sections中包括: 

  • text 文字結 存放指令; 

  • rodata 資料結 readonly; 

  • data 資料結 可讀可寫; 

3>Section頭表(section header table)包含了描述檔案sections的資訊。每個section在這個表中有一個入口;每個入口給出了該section的名字,大小,等等資訊。相當於 索引! 

而程式被載入到記憶體裡面,又是如何分佈的呢?我們看看上圖中:

1正文和初始化的資料和未初始化的資料就是我們所說的資料段,正文即程式碼段; 

2>正文段上面是常量區,常量區上面是全域性變數和靜態變數區,二者佔據的就是初始化的資料和未初始化的資料那部分; 

3>再上面就是堆,動態儲存區,這裡是上增長; 

4>堆上面是棧,存放的是區域性變數,就是區域性變數所在程式碼塊執行完畢後,這塊記憶體會被釋放,這裡棧區是下增長; 

5>命令列引數就是001之類的,環境變數什麼的前面的文章已經講過,有興趣的可以去看看。 

我們知道,記憶體分為動態記憶體和靜態記憶體,我們先講靜態記憶體。

1.1靜態記憶體

儲存模型決定了一個變數的記憶體分配方式和訪問特性,在C語言中主要有三個維度來決定:儲存時期 、作用域 、連結。 

1、儲存時期  儲存時期:變數在記憶體中的保留時間(生命週期)  儲存時期分為兩種情況,關鍵是看變數在程式執行過程中會不會被系統自動回收掉。 

1) 靜態儲存時期 Static  在程式執行過程中一旦分配就不會被自動回收。  通常來說,任何不在函式級別程式碼塊內定義的變數。  無論是否在程式碼塊內,只要採用static關鍵字修飾的變數。 

2) 自動儲存時期 Automatic  除了靜態儲存以外的變數都是自動儲存時期的,或者說只要是在程式碼塊內定義的非static的變數,系統會肚臍自動非配和釋放記憶體; 

2、作用域  作用域:一個變數在定義該變數的自身檔案中的可見性(訪問或者引用)  在C語言中,一共有3中作用域:  1) 程式碼塊作用域  在程式碼塊中定義的變數都具有該程式碼的作用域。從這個變數定義地方開始,到這個程式碼塊結束,該變數是可見的; 

2) 函式原型作用域  出現在函式原型中的變數,都具有函式原型作用域,函式原型作用域從變數定義處一直到原型宣告的末尾。 

3) 檔案作用域  一個在所有函式之外定義的變數具有檔案作用域,具有檔案作用域的變數從它的定義處到包含該定義的檔案結尾處都是可見的; 

3、連結  連結:一個變數在組成程式的所有檔案中的可見性(訪問或者引用);  C語言中一共有三種不同的連結:  1) 外部連結  如果一個變數在組成一個程式的所有檔案中的任何位置都可以被訪問,則稱該變數支援外部連結; 

2) 內部連結  如果一個變數只可以在定義其自身的檔案中的任何位置被訪問,則稱該變數支援內部連結。 

3) 空連結  如果一個變數只是被定義其自身的當前程式碼塊所私有,不能被程式的其他部分所訪問,則成該變數支援空連結 

我們來看一個程式碼示例:

#include <stdio.h>  

int a = 0;// 全域性初始化區    

char *p1; //全域性未初始化區    

int main()    

{    

    int b; //b在棧區  

    char s[] = "abc"; //棧    

    char *p2; //p2在棧區  

    char *p3 = "123456"; //123456\0在常量區,p3在棧上。    

    static int c =0; //全域性(靜態)初始化區  

    p1 = (char *)malloc(10);    

    p2 = (char *)malloc(20);  //分配得來得10和20位元組的區域就在堆區。    

    strcpy(p1, "123456"); //123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。    

}

· 

1.2動態記憶體

當程式執行到需要一個動態分配的變數時,必須向系統申請取得堆中的一塊所需大小的儲存空間,用於儲存該變數。當不在使用該變數時,也就是它的生命結束時,要顯示釋放它所佔用的儲存空間,這樣系統就能對該空間 進行再次分配,做到重複使用有線的資源。下面介紹動態記憶體申請和釋放的函式。

1.2.1 malloc 函式

malloc函式原型:

size是需要動態申請的記憶體的位元組數。若申請成功,函式返回申請到的記憶體的起始地址,若申請失敗,返回NULL。我們看下面這個例子:

使用該函式時,有下面幾點要注意:  1)只關心申請記憶體的大小;  2)申請的是一塊連續的記憶體。記得一定要寫出錯判斷;  3)顯示初始化。即我們不知這塊記憶體中有什麼東西,要對其清零;

1.2.2 free函式

在堆上分配的額記憶體,需要用free函式顯示釋放,函式原型如下:

使用free(),也有下面幾點要注意:  1)必須提供記憶體的起始地址;  呼叫該函式時,必須提供記憶體的起始地址,不能夠提供部分地址,釋放記憶體中的一部分是不允許的。 

2)malloc和free配對使用;  編譯器不負責動態記憶體的釋放,需要程式設計師顯示釋放。因此,malloc與free是配對使用的,避免記憶體洩漏。

p = NULL是必須的,因為雖然這塊記憶體被釋放了,但是p仍指向這塊記憶體,避免下次對p的誤操作; 

3)不允許重複釋放  因為這塊記憶體被釋放後,可能已另分配,這塊區域被別人佔用,如果再次釋放,會造成資料丟失;

1.2.3 其它相關函式

calloc函式分配記憶體需要考慮儲存位置的型別。  realloc函式可以調整一段動態分配記憶體的大小

1.3堆和棧比較

1)申請方式 

stack: 由系統自動分配。 例如,宣告在函式中一個區域性變數 int b; 系統自動在棧中為b開闢空間  heap: 需要程式設計師自己申請,並指明大小,在c中malloc函式 ,如p1 = (char *)malloc(10); 

2)申請後系統的響應  棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。 

堆:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,程式碼中的delete語句才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中。 

3)申請大小的限制  棧:棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。  堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。 

4)申請效率的比較  棧由系統自動分配,速度較快。但程式設計師是無法控制的。  堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便。 

5)堆和棧中的儲存內容  棧:在函式呼叫時,第一個進棧的是主函式中後的下一條指令(函式呼叫語句的下一條可執行語句)的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。 當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續執行。 

堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容由程式設計師安排。 

6)存取效率的比較

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";  

aaaaaaaaaaa是在執行時刻賦值的;  而bbbbbbbbbbb是在編譯時就確定的;  但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。  比如:

對應的彙編程式碼

第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,再根據edx讀取字元,顯然慢了。 

7)最後總結  堆和棧的區別可以用如下的比喻來看出:   

棧就像我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。 

堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。

2記憶體對齊

2.1 #pragma pack(n) 對齊用法詳解

1.什麼是對齊,以及為什麼要對齊  現代計算機中記憶體空間都是按照byte劃分的,從理論上講似乎對任何型別的變數的訪問可以從任何地址開始,但實際情況是在訪問特定變數的時候經常在特定的記憶體地址訪問,這就需要各型別資料按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。 

對齊的作用和原因:各個硬體平臺對儲存空間的處理上有很大的不同。一些平臺對某些特定型別的資料只能從某些特定地址開始存取。其他平臺可能沒有這種情況, 但是最常見的是如果不按照適合其平臺要求對資料存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為 32位系統)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該int資料。顯然在讀取效率上下降很多。這也是空間和時間的博弈。 

待續....