4.1.1.程序運行為什么需要內存
4.1.1.1、計算機程序運行的目的
計算機為什么需要編程?編程已經編了很多年,已經寫了很多程序,為什么還需要另外寫程序?計算機有這個新的程序到底為了什么?
程序的目的是為了去運行,程序運行是為了得到一定的結果。計算機就是用來計算的,所有的計算機程序其實都是在做計算。計算就是在計算數據。所以計算機程序中很重要的部分就是數據。
計算機程序 = 代碼 + 數據 計算機程序運行完得到一個結果,就是說
代碼 + 數據 (經過運行后) = 結果
從宏觀上來理解,代碼就是動作,就是加工數據的動作;數據就是數字,就是被代碼所加工的東西。
那么可以得出結論:程序運行的目的不外乎2個:結果、過程
用函數來類比:函數的形參就是待加工的數據(函數內還需要一些臨時數據,就是局部變量),函數本體就是代碼,函數的返回值就是結果,函數體的執行過程就是過程。
int add(int a, int b)
{
return a + b;
} // 這個函數的執行就是為了得到結果
void add(int a, int b)
{
int c;
c = a + b;
printf("c = %d.\n", c);
} // 這個函數的執行重在過程(重在過程中的printf),返回值不需要
int add(int a, int b)
{
int c;
c = a + b;
printf("c = %d.\n", c);
return c;
} // 這個函數又重結果又重過程
4.1.1.2、計算機程序運行過程
計算機程序的運行過程,其實就是程序中很多個函數相繼運行的過程。程序是由很多個函數組成的,程序的本質就是函數,函數的本質是加工數據的動作。
4.1.1.3、馮諾依曼結構和哈佛結構
馮諾依曼結構是:數據和代碼放在一起。
哈佛結構是:數據和代碼分開存在。
什么是代碼:函數
什么是數據:全局變量、局部變量
在S5PV210中運行的linux系統上,運行應用程序時:這時候所有的應用程序的代碼和數據都在DRAM,所以這種結構就是馮諾依曼結構;在單片機中,我們把程序代碼燒寫到Flash(NorFlash)中,然后程序在Flash中原地運行,程序中所涉及到的數據(全局變量、局部變量)不能放在Flash中,必須放在RAM(SRAM)中。這種就叫哈佛結構。
4.1.1.4、動態內存DRAM和靜態內存SRAM
DRAM是動態內存,SRAM是靜態內存。詳細細節自己baidu
4.1.1.5、總結:為什么需要內存呢
內存是用來存儲可變數據的,數據在程序中表現為全局變量、局部變量等(在gcc中,其實常量也是存儲在內存中的)(大部分單片機中,常量是存儲在flash中的,也就是在代碼段),對我們寫程序來說非常重要,對程序運行更是本質相關。
所以內存對程序來說幾乎是本質需求。越簡單的程序需要越少的內存,而越龐大越復雜的程序需要更多的內存。內存管理是我們寫程序時很重要的話題。我們以前學過的了解過的很多編程的關鍵其實都是為了內存,譬如說數據結構(數據結構是研究數據如何組織的,數據是放在內存中的)和算法(算法是為了用更優秀更有效的方法來加工數據,既然跟數據有關就離不開內存)。
4.1.1.6、深入思考:如何管理內存(無OS時,有OS時)
對于計算機來說,內存容量越大則可能性越大,所以大家都希望自己的電腦內存更大。我們寫程序時如何管理內存就成了很大的問題。如果管理不善,可能會造成程序運行消耗過多的內存,這樣遲早內存都被你這個程序吃光了,當沒有內存可用時程序就會崩潰。所以內存對程序來說是一種資源,所以管理內存對程序來說是一個重要技術和話題。
先從操作系統角度講:操作系統掌握所有的硬件內存,因為內存很大,所以操作系統把內存分成1個1個的頁面(其實就是一塊,一般是4KB),然后以頁面為單位來管理。頁面內用更細小的方式來以字節為單位管理。操作系統內存管理的原理非常麻煩、非常復雜、非常不人性化。那么對我們這些使用操作系統的人來說,其實不需要了解這些細節。操作系統給我們提供了內存管理的一些接口,我們只需要用API即可管理內存。
譬如在C語言中使用malloc free這些接口來管理內存。
沒有操作系統時:在沒有操作系統(其實就是裸機程序)中,程序需要直接操作內存,編程者需要自己計算內存的使用和安排。如果編程者不小心把內存用錯了,錯誤結果需要自己承擔。
再從語言角度來講:不同的語言提供了不同的操作內存的接口。
譬如匯編:根本沒有任何內存管理,內存管理全靠程序員自己,匯編中操作內存時直接使用內存地址(譬如0xd0020010),非常麻煩;
譬如C語言:C語言中編譯器幫我們管理直接內存地址,我們都是通過編譯器提供的變量名等來訪問內存的,操作系統下如果需要大塊內存,可以通過API(malloc free)來訪問系統內存。裸機程序中需要大塊的內存需要自己來定義數組等來解決。
譬如C++語言:C++語言對內存的使用進一步封裝。我們可以用new來創建對象(其實就是為對象分配內存),然后使用完了用delete來刪除對象(其實就是釋放內存)。所以C++語言對內存的管理比C要高級一些,容易一些。但是C++中內存的管理還是靠程序員自己來做。如果程序員new了一個對象,但是用完了忘記delete就會造成這個對象占用的內存不能釋放,這就是內存泄漏。
Java/C#等語言:這些語言不直接操作內存,而是通過虛擬機來操作內存。這樣虛擬機作為我們程序員的代理,來幫我們處理內存的釋放工作。如果我的程序申請了內存,使用完成后忘記釋放,則虛擬機會幫我釋放掉這些內存。聽起來似乎C# java等語言比C/C++有優勢,但是其實他這個虛擬機回收內存是需要付出一定代價的,所以說語言沒有好壞,只有適應不適應。當我們程序對性能非常在乎的時候(譬如操作系統內核)就會用C/C++語言;當我們對開發程序的速度非常在乎的時候,就會用Java/C#等語言。
4.1.3.位、字節、半字、字的概念和內存位寬
4.1.3.1、什么是內存?(硬件和邏輯兩個角度)
從硬件角度:內存實際上是電腦的一個配件(一般叫內存條)。根據不同的硬件實現原理還可以把內存分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后來的DDR1、DDR2·····、LPDDR)
從邏輯角度:內存是這樣一種東西,它可以隨機訪問(隨機訪問的意思是只要給一個地址,就可以訪問這個內存地址)、并且可以讀寫(當然了邏輯上也可以限制其為只讀或者只寫);內存在編程中天然是用來存放變量的(就是因為有了內存,所以C語言才能定義變量,C語言中的一個變量實際就對應內存中的一個單元)。
4.1.3.2、內存的邏輯抽象圖(內存的編程模型)
從邏輯角度來講,內存實際上是由無限多個內存單元格組成的,每個單元格有一個固定的地址叫內存地址,這個內存地址和這個內存單元格唯一對應且永久綁定。
以大樓來類比內存是最合適的。邏輯上的內存就好象是一棟無限大的大樓,內存的單元格就好象大樓中的一個個小房間。每個內存單元格的地址就好象每個小房間的房間號。內存中存儲的內容就好象住在房間中的人一樣。
邏輯上來說,內存可以有無限大(因為數學上編號永遠可以增加,無盡頭)。但是現實中實際的內存大小是有限制的,譬如32位的系統(32位系統指的是32位數據線,但是一般地址線也是32位,這個地址線32位決定了內存地址只能有32位二進制,所以邏輯上的大小為2的32次方)內存限制就為4G。實際上32位的系統中可用的內存是小于等于4G的(譬如我32位CPU裝32位windows,但實際電腦只有512M內存)
4.1.3.3、位和字節
內存單元的大小單位有4個:位(1bit) 字節(8bit) 半字(一般是16bit) 字(一般是32bit)
在所有的計算機、所有的機器中(不管是32位系統還是16位系統還是以后的64位系統),位永遠都是1bit,字節永遠都是8bit。
4.1.3.4、字和半字
歷史上曾經出現過16位系統、32位系統、64位系統三種,而且操作系統還有windows、linux、iOS等很多,所以很多的概念在歷史上曾經被混亂的定義過。
建議大家對字、半字、雙字這些概念不要詳細區分,只要知道這些單位具體有多少位是依賴于平臺的。實際工作中在每種平臺上先去搞清楚這個平臺的定義(字是多少位,半字永遠是字的一半,雙字永遠是字的2倍大小)。
編程時一般根本用不到字這個概念,那我們區分這個概念主要是因為有些文檔中會用到這些概念,如果不加區別可能會造成你對程序的誤解。
在linux+ARM這個軟硬件平臺上(我們嵌入式核心課的所有課程中),字是32位的。
4.1.3.5、內存位寬(硬件和邏輯兩個角度)
從硬件角度講:硬件內存的實現本身是有寬度的,也就是說有些內存條就是8位的,而有些就是16位的。那么需要強調的是內存芯片之間是可以并聯的,通過并聯后即使8位的內存芯片也可以做出來16位或32位的硬件內存。
從邏輯角度講:內存位寬在邏輯上是任意的,甚至邏輯上存在內存位寬是24位的內存(但是實際上這種硬件是買不到的,也沒有實際意義)。從邏輯角度來講不管內存位寬是多少,我就直接操作即可,對我的操作不構成影響。但是因為你的操作不是純邏輯而是需要硬件去執行的,所以不能為所欲為,所以我們實際的很多操作都是受限于硬件的特性的。譬如24位的內存邏輯上和32位的內存沒有任何區別,但實際硬件都是32位的,都要按照32位硬件的特性和限制來干活。
4.1.4.內存編址和尋址、內存對齊
4.1.4.1、內存編址方法
內存在邏輯上就是一個一個的格子,這些格子可以用來裝東西(里面裝的東西就是內存中存儲的數),每個格子有一個編號,這個編號就是內存地址,這個內存地址(一個數字)和這個格子的空間(實質是一個空間)是一一對應且永久綁定的。這就是內存的編址方法。
在程序運行時,計算機中CPU實際只認識內存地址,而不關心這個地址所代表的空間在哪里,怎么分布這些實體問題。因為硬件設計保證了按照這個地址就一定能找到這個格子,所以說內存單元的2個概念:地址和空間是內存單元的兩個方面。
4.1.4.2、關鍵:內存編址是以字節為單位的
我隨便給一個數字(譬如說7),然后說這個數字是一個內存地址,然后問你這個內存地址對應的空間多大?這個大小是固定式,就是一個字節(8bit)。
如果把內存比喻位一棟大樓,那么這個樓里面的一個一個房間就是一個一個內存格子,這個格子的大小是固定的8bit,就好象這個大樓里面所有的房間戶型是一樣的。
4.1.4.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.1.4.4、內存對齊
我們在C中int a;定義一個int類型變量,在內存中就必須分配4個字節來存儲這個a。有這么2種不同內存分配思路和策略:
第一種:0 1 2 3 對齊訪問
第二種:1 2 3 4 或者 2 3 4 5 或者 3 4 5 6 非對齊訪問
內存的對齊訪問不是邏輯的問題,是硬件的問題。從硬件角度來說,32位的內存它 0 1 2 3四個單元本身邏輯上就有相關性,這4個字節組合起來當作一個int硬件上就是合適的,效率就高。
對齊訪問很配合硬件,所以效率很高;非對齊訪問因為和硬件本身不搭配,所以效率不高。(因為兼容性的問題,一般硬件也都提供非對齊訪問,但是效率要低很多。)
4.1.4.5、從內存編址看數組的意義
4.1.5.C語言如何操作內存
4.1.5.1、C語言對內存地址的封裝(用變量名來訪問內存、數據類型的含義、函數名的含義)
譬如在C語言中 int a; a = 5; a += 4; // a == 9;
結合內存來解析C語言語句的本質:
int a; // 編譯器幫我們申請了1個int類型的內存格子(長度是4字節,地址是確定的,但是只有編譯器知道,我們是不知道的,也不需要知道。),并且把符號a和這個格子綁定。
a = 5; // 編譯器發現我們要給a賦值,就會把這個值5丟到符號a綁定的那個內存格子中。
a += 4; // 編譯器發現我們要給a加值,a += 4 等效于 a = a + 4;編譯器會先把a原來的值讀出來,然后給這個值加4,再把加之后的和寫入a里面去。
C語言中數據類型的本質含義是:表示一個內存格子的長度和解析方法。
數據類型決定長度的含義:我們一個內存地址(0x30000000),本來這個地址只代表1個字節的長度,但是實際上我們可以通過給他一個類型(int),讓他有了長度(4),這樣這個代表內存地址的數字(0x30000000)就能表示從這個數字(0x30000000)開頭的連續的n(4)個字節的內存格子了(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)。
數據類型決定解析方法的含義:譬如我有一個內存地址(0x30000000),我們可以通過給這個內存地址不同的類型來指定這個內存單元格子中二進制數的解析方法。譬如我 (int)0x30000000,含義就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)這4個字節連起來共同存儲的是一個int型數據;那么我(float)0x30000000,含義就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)這4個字節連起來共同存儲的是一個float型數據;
之前講過一個很重要的概念:內存單元格子的編址單位是字節。
(int *)0;
(float *)0;
(short)0;
(char)0;
int a; // int a;時編譯器會自動給a分配一個內存地址,譬如說是0x12345678
(int *)a; // 等價于(int *)0x12345678
(float *)a;
C語言中,函數就是一段代碼的封裝。函數名的實質就是這一段代碼的首地址。所以說函數名的本質也是一個內存地址。
4.1.5.2、用指針來間接訪問內存
關于類型(不管是普通變量類型int float等,還是指針類型int * float *等),只要記住:
類型只是對后面數字或者符號(代表的是內存地址)所表征的內存的一種長度規定和解析方法規定而已。
C語言中的指針,全名叫指針變量,指針變量其實很普通變量沒有任何區別。譬如int a和int *p其實沒有任何區別,a和p都代表一個內存地址(譬如是0x20000000),但是這個內存地址(0x20000000)的長度和解析方法不同。a是int型所以a的長度是4字節,解析方法是按照int的規定來的;p是int *類型,所以長度是4字節,解析方法是int *的規定來的(0x20000000開頭的連續4字節中存儲了1個地址,這個地址所代表的內存單元中存放的是一個int類型的數)。
4.1.5.3、指針類型的含義
4.1.5.4、用數組來管理內存
數組管理內存和變量其實沒有本質區別,只是符號的解析方法不同。(普通變量、數組、指針變量其實都沒有本質差別,都是對內存地址的解析,只是解析方法不一樣)。
int a; // 編譯器分配4字節長度給a,并且把首地址和符號a綁定起來。
int b[10]; // 編譯器分配40個字節長度給b,并且把首元素首地址和符號b綁定起來。
數組中第一個元素(a[0])就稱為首元素;每一個元素類型都是int,所以長度都是4,其中第一個字節的地址就稱為首地址;首元素a[0]的首地址就稱為首元素首地址。
4.1.6.內存管理之結構體
4.1.6.1、數據結構這門學問的意義
數據結構就是研究數據如何組織(在內存中排布),如何加工的學問。
4.1.6.2、最簡單的數據結構:數組
為什么要有數組?因為程序中有好多個類型相同、意義相關的變量需要管理,這時候如果用單獨的變量來做程序看起來比較亂,用數組來管理會更好管理。
譬如 int ages[20];
4.1.6.3、數組的優勢和缺陷
優勢:數組比較簡單,訪問用下標,可以隨機訪問。
缺陷:1 數組中所有元素類型必須相同;2 數組大小必須定義時給出,而且一旦確定不能再改。
4.1.6.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的各個元素類型不完全相同,所以必須用結構體,沒法用數組。
4.1.6.5、題外話:結構體內嵌指針實現面向對象
面向過程與面向對象。
總的來說:C語言是面向過程的,但是C語言寫出的linux系統是面向對象的。
非面向對象的語言,不一定不能實現面向對象的代碼。只是說用面向對象的語言來實現面向對象要更加簡單一些、直觀一些、無腦一些。
用C++、Java等面向對象的語言來實現面向對象簡單一些,因為語言本身幫我們做了很多事情;但是用C來實現面向對象很麻煩,看起來也不容易理解,這就是為什么大多數人學過C語言卻看不懂linux內核代碼的原因。
struct s
{
int age; // 普通變量
void (*pFunc)(void); // 函數指針,指向 void func(void)這類的函數
};
使用這樣的結構體就可以實現面向對象。
這樣包含了函數指針的結構體就類似于面向對象中的class,結構體中的變量類似于class中的成員變量,結構體中的函數指針類似于class中的成員方法。
(引用自朱有鵬老師課件)