1. 程式人生 > >C語言高階篇 - 5.記憶體

C語言高階篇 - 5.記憶體

1、馮諾依曼結構和哈佛結構

     (1)馮諾依曼結構是:資料和程式碼放在一起。

     (2)哈佛結構是:資料和程式碼分開存在。

     (3)什麼是程式碼:函式

     (4)什麼是資料:全域性變數、區域性變數

     (5)在S5PV210中執行的linux系統上,執行應用程式時:這時候所有的應用程式的程式碼和資料都在DRAM,所以這種結構就是馮諾依曼結構;在微控制器中,我們把程式程式碼燒寫到Flash(NorFlash)中,然後程式在Flash中原地執行,程式中所涉及到的資料(全域性變數、區域性變數)不能放在Flash中,必須放在RAM(SRAM)中。這種就叫哈佛結構。

 

2、動態記憶體DRAM和靜態記憶體SRAM

 

3、記憶體和資料型別的關係

        C語言中的基本資料型別有:char short int long float double 

        int 整形(整數型別,這個整就體現在它和CPU本身的資料位寬是一樣的)譬如32位的CPU,整形就是32位,int就是32位。

        資料型別和記憶體的關係就在於:

        資料型別是用來定義變數的,而這些變數需要儲存、運算在記憶體中。所以資料型別必須和記憶體相匹配才能獲得最好的效能,否則可能不工作或者效率低下。

        在32位系統中定義變數最好用int,因為這樣效率高。原因就在於32位的系統本身配合記憶體等也是32位,這樣的硬體配置天生適合定義32位的int型別變數,效率最高。也能定義8位的char型別變數或者16位的short型別變數,但是實際上訪問效率不高。

        在很多32位環境下,我們實際定義bool型別變數(實際只需要1個bit就夠了)都是用int來實現bool的。也就是說我們定義一個bool b1;時,編譯器實際幫我們分配了32位的記憶體來儲存這個bool變數b1。編譯器這麼做實際上浪費了31位的記憶體,但是好處是效率高。

        問題:實際程式設計時要以省記憶體為大還是要以執行效率為重?答案是不定的,看具體情況。很多年前記憶體很貴機器上記憶體都很少,那時候寫程式碼以省記憶體為主。現在隨著半導體技術的發展記憶體變得很便宜了,現在的機器都是高配,不在乎省一點記憶體,而效率和使用者體驗變成了關鍵。所以現在寫程式大部分都是以效率為重。

 

4、結構體

        結構體發明出來就是為了解決陣列的第一個缺陷:陣列中所有元素型別必須相同

        我們要管理3個學生的年齡(int型別),怎麼辦?

        第一種解法:用陣列        int ages[3];

        第二種解法:用結構體    

    struct ages

    {

        int  age1;

        int  age2;

        int  age3;

    };

    struct ages age;

    分析總結:在這個示例中,陣列要比結構體好。但是不能得出結論說陣列就比結構體好,在包中元素型別不同時就只能用結構體而不能用陣列了。

    struct people

    {

        int age;            // 人的年齡

        char name[20];        // 人的姓名

        int height;            // 人的身高

    };

        因為people的各個元素型別不完全相同,所以必須用結構體,沒法用陣列。

 

5、題外話:結構體內嵌指標實現面向物件

        (1)面向過程與面向物件。

            總的來說:C語言是面向過程的,但是C語言寫出的linux系統是面向物件的。

        (2)非面向物件的語言,不一定不能實現面向物件的程式碼。只是說用面向物件的語言來實現面向物件要更加簡單一些、直觀一些、無腦一些。

          用C++、Java等面向物件的語言來實現面向物件簡單一些,因為語言本身幫我們做了很多事情;但是用C來實現面向物件很麻煩,看起來也不容易理解,這就是為什麼大多數人學過C語言卻看不懂linux核心程式碼的原因。

struct s

{

    int age;                    // 普通變數

    void (*pFunc)(void);        // 函式指標,指向 void func(void)這類的函式

};

        使用這樣的結構體就可以實現面向物件。

        這樣包含了函式指標的結構體就類似於面向物件中的class,結構體中的變數類似於class中的成員變數,結構體中的函式指標類似於class中的成員方法。

 

6、記憶體管理之棧(stack)

        (1)什麼是棧

        棧是一種資料結構,C語言中使用棧來儲存區域性變數。棧是被髮明出來管理記憶體的。

        (2)棧管理記憶體的特點(小記憶體、自動化)

              先進後出 FILO    first in last out        棧

              先進先出 FIFO   first in first out      佇列

        棧的特點是入口即出口,只有一個口,另一個口是堵死的。所以先進去的必須後出來。

佇列的特點是入口和出口都有,必須從入口進去,從出口出來,所以先進去的必須先出來,否則就堵住後面的。

 

        (3)棧的應用舉例:區域性變數

         C語言中的區域性變數是用棧來實現的。

         我們在C中定義一個區域性變數時(int a),編譯器會在棧中分配一段空間(4位元組)給這個區域性變數用(分配時棧頂指標會移動給出空間,給區域性變數a用的意思就是,將這4位元組的棧記憶體的記憶體地址和我們定義的區域性變數名a給關聯起來),對應棧的操作是入棧。

        注意:這裡棧指標的移動和記憶體分配是自動的(棧自己完成,不用我們寫程式碼去操作)。

        然後等我們函式退出的時候,區域性變數要滅亡。對應棧的操作是彈棧(出棧)。出棧時也是棧頂指標移動將棧空間中與a關聯的那4個位元組空間釋放。這個動作也是自動的,也不用人寫程式碼干預。

 

        (4)棧的優點:棧管理記憶體,好處是方便,分配和最後回收都不用程式設計師操心,C語言自動完成。

        分析一個細節:C語言中,定義區域性變數時如果未初始化,則值是隨機的,為什麼?

        定義區域性變數,其實就是在棧中通過移動棧指標來給程式提供一個記憶體空間和這個區域性變數名繫結。因為這段記憶體空間在棧上,而棧記憶體是反覆使用的(髒的,上次用完沒清零的),所以說使用棧來實現的區域性變數定義時如果不顯式初始化,值就是髒的。如果你顯式初始化怎麼樣?

        C語言是通過一個小手段來實現區域性變數的初始化的。

int a = 15;        // 區域性變數定義時初始化

C語言編譯器會自動把這行轉成:

int a;            // 區域性變數定義

a = 15;            // 普通的賦值語句

 

        (5)棧的約束(預定棧大小不靈活,怕溢位)

        首先,棧是有大小的。所以棧記憶體大小不好設定。如果太小怕溢位,太大怕浪費記憶體。(這個缺點有點像陣列)

        其次,棧的溢位危害很大,一定要避免。所以我們在C語言中定義區域性變數時不能定義太多或者太大(譬如不能定義區域性變數時 int a[10000]; 使用遞迴來解決問題時一定要注意遞迴收斂)

 

7、記憶體管理之堆

        (1)、什麼是堆

        堆(heap)是一種記憶體管理方式。記憶體管理對作業系統來說是一件非常複雜的事情,因為首先記憶體容量很大,其次記憶體需求在時間和大小塊上沒有規律(作業系統上執行著的幾十、幾百、幾千個程序隨時都會申請或者釋放記憶體,申請或者釋放的記憶體塊大小隨意)。

       堆這種記憶體管理方式特點就是自由(隨時申請、釋放;大小塊隨意)。堆記憶體是作業系統劃歸給堆管理器(作業系統中的一段程式碼,屬於作業系統的記憶體管理單元)來管理的,然後向使用者(使用者程序)提供API(malloc和free)來使用堆記憶體。

        我們什麼時候使用堆記憶體?需要記憶體容量比較大時,需要反覆使用及釋放時,很多資料結構(譬如連結串列)的實現都要使用堆記憶體。

        (2)、堆管理記憶體的特點(大塊記憶體、手工分配&使用&釋放)

        特點一:容量不限(常規使用的需求容量都能滿足)。

        特點二:申請及釋放都需要手工進行,手工進行的含義就是需要程式設計師寫程式碼明確進行申請malloc及釋放free。如果程式設計師申請記憶體並使用後未釋放,這段記憶體就丟失了(在堆管理器的記錄中,這段記憶體仍然屬於你這個程序,但是程序自己又以為這段記憶體已經不用了,再用的時候又會去申請新的記憶體塊,這就叫吃記憶體),稱為記憶體洩漏。在C/C++語言中,記憶體洩漏是最嚴重的程式bug,這也是別人認為Java/C#等語言比C/C++優秀的地方。

        (3)、C語言操作堆記憶體的介面(malloc free)

堆記憶體釋放時最簡單,直接呼叫free釋放即可。    void free(void *ptr);

堆記憶體申請時,有3個可選擇的類似功能的函式:malloc, calloc, realloc

void *malloc(size_t size);

void *calloc(size_t nmemb, size_t size);    // nmemb個單元,每個單元size位元組

void *realloc(void *ptr, size_t size);        // 改變原來申請的空間的大小的

 

譬如要申請10個int元素的記憶體:

malloc(40);            malloc(10*sizeof(int));

calloc(10, 4);        calloc(10, sizeof(int));

 

        陣列定義時必須同時給出陣列元素個數(陣列大小),而且一旦定義再無法更改。在Java等高階語言中,有一些語法技巧可以更改陣列大小,但其實這只是一種障眼法。它的工作原理是:先重新建立一個新的陣列大小為要更改後的陣列,然後將原陣列的所有元素複製進新的陣列,然後釋放掉原陣列,最後返回新的陣列給使用者;

 

        堆記憶體申請時必須給定大小,然後一旦申請完成大小不變,如果要變只能通過realloc介面。realloc的實現原理類似於上面說的Java中的可變大小的陣列的方式。

(4)、堆的優勢和劣勢(管理大塊記憶體、靈活、容易記憶體洩漏)

    優勢:靈活;

    劣勢:需要程式設計師去處理各種細節,所以容易出錯,嚴重依賴於程式設計師的水平。

 

8、複雜資料結構

        (1)、連結串列、雜湊表、二叉樹、圖等

        連結串列是最重要的,連結串列在linux核心中使用非常多,驅動、應用編寫很多時候都需要使用連結串列。所以對連結串列必須掌握,掌握到:會自己定義結構體來實現連結串列、會寫連結串列的節點插入(前插、後插)、節點刪除、節點查詢、節點遍歷等。(至於像逆序這些很少用,掌握了前面那幾個這個也不難)。

        雜湊表不是很常用,一般不需要自己寫實現,而直接使用別人實現的雜湊表比較多。對我們來說最重要的是要明白雜湊表的原理、從而知道雜湊表的特點,從而知道什麼時候該用雜湊表,當看到別人用了雜湊表的時候要明白別人為什麼要用雜湊表、合適不合適?有沒有更好的選擇?

        二叉樹、圖等。對於這些複雜資料結構,不要太當回事。這些複雜資料結構用到的概率很小(在嵌入式開發中),其實這些資料結構被髮明出來就是為了解決特定問題的,你不處理特定問題根本用不到這些,沒必要去研究。

 

        (2)、為什麼需要更復雜的資料結構

        因為現實中的實際問題是多種多樣的,問題的複雜度不同,所以需要解決問題的演算法和資料結構也不同。所以當你處理什麼複雜度的問題,就去研究針對性解決的資料結構和演算法;當你沒有遇到此類問題(或者你工作的領域根本跟這個就沒關係)時就不要去管了。

 

        (3)、資料結構和演算法的關係

        資料結構的發明都是為了配合一定的演算法;演算法是為了處理具體問題,演算法的實現依賴於相應的資料結構。

        當前我們說的演算法和純數學是不同的(演算法是基於數學的,大學計算機系研究生博士生很多本科都是數學相關專業的),因為計算機演算法要求以數學演算法為指導,並且結合計算機本身的特點來改進,最終實現一個在計算機上可以執行的演算法(意思就是用程式碼可以表示的演算法)。

 

        (4)、應該怎樣學習這部分?

從上面表述大家應該明白以下事實:

    1. 資料結構和演算法是相輔相成的,要一起研究。

    2. 資料結構和演算法對嵌入式來說不全是重點,不要盲目的跑去研究這個。

    3. 一般在實際應用中,實現資料結構和演算法的人和使用資料結構和演算法的人是分開的。實際中有一部分人的工作就是研究資料結構和演算法,並且試圖用程式碼來實現這些演算法(表現為庫);其他做真正工作的人要做的就是理解、明白這些演算法和資料結構的意義、優劣、特徵,然後在合適的時候選擇合適的資料結構和演算法來解決自己碰到的實際問題。

 

舉個例子:linux核心在字元裝置驅動管理時,使用了雜湊表(hash table,散列表)。所以字元裝置驅動的很多特點都和雜湊表的特點有關。