内存的使用
內容整理于朱有鵬老師的課程。
1、馮諾依曼結構和哈佛結構
- 馮諾依曼結構是數據和代碼放在一起,哈佛結構是數據和代碼分開存在。
- 什么是代碼:函數。什么是數據:全局變量、局部變量。
- 在S5PV210中運行的linux系統上,運行應用程序時,所有的應用程序的代碼和數據都在DRAM,所以這種結構就是馮諾依曼結構。
- 在單片機中,程序代碼燒寫到Flash(NorFlash)中,然后程序在Flash中原地運行,程序中所涉及到的數據(全局變量、局部變量)放在RAM(SRAM)中。這種就叫哈佛結構。
2、動態內存DRAM和靜態內存SRAM
- DRAM是動態內存,SRAM是靜態內存。
3、為什么需要內存
- 內存是用來存儲可變數據的,數據在程序中表現為全局變量、局部變量等(在gcc中,其實常量也是存儲在內存中的)(大部分單片機中,常量是存儲在flash中的,也就是在代碼段),對我們寫程序來說非常重要,對程序運行更是本質相關。所以內存對程序來說幾乎是本質需求。
4、如何管理內存
(1)有無操作系統
- 有操作系統:操作系統掌握所有的硬件內存,因為內存很大,所以操作系統把內存分成1個1個的頁面(其實就是一塊,一般是4KB),然后以頁面為單位來管理。頁面內用更細小的方式來以字節為單位管理。操作系統內存管理的原理非常麻煩、非常復雜、非常不人性化。操作系統給我們提供了內存管理的一些接口,我們只需要用API即可管理內存。譬如在C語言中使用malloc free這些接口來管理內存。
- 無操作系統時:裸機程序中,程序需要直接操作內存,編程者需要自己計算內存的使用和安排。
- 不同的語言提供了不同的操作內存的接口。
- 譬如匯編根本沒有任何內存管理,內存管理全靠程序員自己,匯編中操作內存時直接使用內存地址(譬如0xd0020010),非常麻煩;
- 譬如C語言中編譯器管理直接內存地址,我們都是通過編譯器提供的變量名等來訪問內存的,操作系統下如果需要大塊內存,可以通過API(malloc free)來訪問系統內存。
- 譬如C++語言對內存的使用進一步封裝。我們可以用new來創建對象(其實就是為對象分配內存),然后使用完了用delete來刪除對象(其實就是釋放內存)。所以C++語言對內存的管理比C要高級一些,容易一些。但是C++中內存的管理還是靠程序員自己來做。如果程序員new了一個對象,但是用完了忘記delete就會造成這個對象占用的內存不能釋放,這就是內存泄漏。
- Java/C#等語言不直接操作內存,而是通過虛擬機來操作內存。這樣虛擬機作為我們程序員的代理,來幫我們處理內存的釋放工作。如果程序申請了內存,使用完成后忘記釋放,則虛擬機會釋放掉這些內存。
5、位、字節、半字、字和內存位寬
(1)什么是內存?
- 從硬件角度:內存是電腦的一個配件(內存條)。根據不同的硬件實現原理又分成SRAM和DRAM(DRAM又有好多代,譬如最早的SDRAM,后來的DDR1……LPDDR)。
- 從邏輯角度:內存可以隨機訪問(隨機訪問的意思是只要給一個地址,就可以訪問這個內存地址),并且可以讀寫。內存是用來存放變量的(因為有了內存,所以C語言中才能定義變量,C語言中的一個變量對應內存中的一個單元)。
- 邏輯上來說,內存可以有無限大(因為數學上編號永遠可以增加,無盡頭)。但是實際的內存大小是有限制的,譬如32位的系統(32位系統指的是32位數據線,但是一般地址線也是32位,這個地址線32位決定了內存地址只能有32位二進制,所以邏輯上的大小為2的32次方)內存限制就為4G。
- 在所有的計算機、所有的機器中(不管是32位系統還是16位系統還是以后的64位系統),位永遠都是1bit,字節永遠都是8bit。
- 這些單位具體有多少位是依賴于平臺的。實際工作中在每種平臺上先去搞清楚這個平臺的定義(字是多少位,半字永遠是字的一半,雙字永遠是字的2倍大小)。
- 在linux+ARM這個軟硬件平臺上(我們嵌入式核心課的所有課程中),字是32位的。
- 從硬件角度講:硬件內存的實現本身是有寬度的,即有些內存條是8位的,有些是16位的。內存芯片之間可以并聯,通過并聯后即使8位的內存芯片也可以做出來16位或32位的硬件內存。
- 從邏輯角度講:內存位寬在邏輯上是任意的,甚至邏輯上存在內存位寬是24位的內存(但是實際上這種硬件是買不到的,也沒有實際意義)。
6、內存編址和尋址、內存對齊
(1)內存編址是以字節為單位的
(2)內存和數據類型的關系
- 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位的內存,但是好處是效率高。
(3)內存對齊
- 在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硬件上就是合適的,效率就高。對齊訪問很配合硬件,所以效率很高;非對齊訪問因為和硬件本身不搭配,所以效率不高。(因為兼容性的問題,一般硬件也都提供非對齊訪問,但是效率要低很多。)
7、C語言如何操作內存
(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語言中,函數就是一段代碼的封裝。函數名的實質就是這一段代碼的首地址。所以說函數名的本質也是一個內存地址。
------------------------------------------------------------------------------------------------------
(2)用指針來間接訪問內存
- 類型只是對后面數字或者符號(代表的是內存地址)所表征的內存的一種長度規定和解析方法規定而已。
- C語言中的指針,全名叫指針變量,指針變量其實很普通變量沒有任何區別。譬如int a和int *p其實沒有任何區別,a和p都代表一個內存地址(譬如是0x20000000),但是這個內存地址(0x20000000)的長度和解析方法不同。a是int型所以a的長度是4字節,解析方法是按照int的規定來的;p是int *類型,所以長度是4字節,解析方法是int *的規定來的(0x20000000開頭的連續4字節中存儲了1個地址,這個地址所代表的內存單元中存放的是一個int類型的數)。
(3)用數組來管理內存
- 數組管理內存和變量其實沒有本質區別,只是符號的解析方法不同。(普通變量、數組、指針變量其實都沒有本質差別,都是對內存地址的解析,只是解析方法不一樣)。
- int a; // 編譯器分配4字節長度給a,并且把首地址和符號a綁定起來。
- int b[10]; // 編譯器分配40個字節長度給b,并且把首元素首地址和符號b綁定起來。數組中第一個元素(b[0])就稱為首元素;每一個元素類型都是int,所以長度都是4,其中第一個字節的地址就稱為首地址;首元素b[0]的首地址就稱為首元素首地址。
8、內存管理之結構體
(1)數組的優勢和缺陷
- 優勢:數組比較簡單,訪問用下標,可以隨機訪問。
- 缺陷:1 數組中所有元素類型必須相同;2 數組大小必須定義時給出,而且一旦確定不能再改。
- 結構體發明出來就是為了解決數組的第一個缺陷:數組中所有元素類型必須相同
- C語言是面向過程的,但是C語言寫出的linux系統是面向對象的。
- 非面向對象的語言,不一定不能實現面向對象的代碼。只是說用面向對象的語言來實現面向對象要更加簡單一些、直觀一些。用C++、Java等面向對象的語言來實現面向對象簡單一些,因為語言本身幫我們做了很多事情
struct s {int age; // 普通變量void (*pFunc)(void); // 函數指針,指向 void func(void)這類的函數 };//使用這樣的結構體就可以實現面向對象。 //這樣包含了函數指針的結構體就類似于面向對象中的class ///結構體中的變量類似于class中的成員變量,結構體中的函數指針類似于class中的成員方法。
9、內存管理之棧(stack)
(1)什么是棧
- 棧是一種數據結構,C語言中使用棧來保存局部變量。棧是被發明出來管理內存的。
(2)棧管理內存的特點(小內存、自動化)
- 先進后出 FILO:first in last out,棧;
- 先進先出 FIFO:first in first out,隊列;
- C語言中的局部變量是用棧來實現的。
- 我們在C中定義一個局部變量時(int a),編譯器會在棧中分配一段空間(4字節)給這個局部變量用(分配時棧頂指針會移動給出空間,給局部變量a用的意思就是,將這4字節的棧內存的內存地址和我們定義的局部變量名a給關聯起來),對應棧的操作是入棧。函數退出的時候,局部變量要滅亡,對應棧的操作是彈棧(出棧)。出棧時也是棧頂指針移動將??臻g中與a關聯的那4個字節空間釋放。
- 注意這里棧指針的移動和內存分配是自動的(棧自己完成,不用我們寫代碼去操作)。
(4)棧的優點
- 好處是方便,分配和最后回收都不用程序員操心,C語言自動完成。
- C語言中,定義局部變量時如果未初始化,則值是隨機的,為什么?定義局部變量,其實就是在棧中通過移動棧指針來給程序提供一個內存空間和這個局部變量名綁定。因為這段內存空間在棧上,而棧內存是反復使用的(臟的,上次用完沒清零的),所以說使用棧來實現的局部變量定義時如果不顯式初始化,值就是臟的。
- 如果你顯式初始化怎么樣?C語言是通過一個小手段來實現局部變量的初始化的,即:
- int a = 15;// 局部變量定義時初始化
- C語言編譯器會自動把這行轉成:i
- nt a;// 局部變量定義
- a = 15;// 普通的賦值語句
- 棧是有大小的。所以棧內存大小不好設置。如果太小怕溢出,太大怕浪費內存。(這個缺點有點像數組);
- 棧的溢出危害很大,一定要避免。
- 我們在C語言中定義局部變量時不能定義太多或者太大(譬如不能定義局部變量時 int a[10000]; 使用遞歸來解決問題時一定要注意遞歸收斂)
10、內存管理之堆
(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
- 數組定義時必須同時給出數組元素個數(數組大小),而且一旦定義再無法更改。在Java等高級語言中,有一些語法技巧可以更改數組大小,但其實這只是一種障眼法。它的工作原理是:先重新創建一個新的數組大小為要更改后的數組,然后將原數組的所有元素復制進新的數組,然后釋放掉原數組,最后返回新的數組給用戶.
- 堆內存申請時必須給定大小,然后一旦申請完成大小不變,如果要變只能通過realloc接口。realloc的實現原理類似于上面說的Java中的可變大小的數組的方式。
- 管理大塊內存、靈活、容易內存泄漏。
11、復雜數據結構
(1)鏈表、哈希表、二叉樹、圖等
- 鏈表在linux內核中使用非常多,驅動、應用編寫很多時候都需要使用鏈表。所以對鏈表必須掌握,掌握到:會自己定義結構體來實現鏈表、會寫鏈表的節點插入(前插、后插)、節點刪除、節點查找、節點遍歷等。(至于像逆序這些很少用,掌握了前面那幾個這個也不難)。
- 哈希表不是很常用,一般不需要自己寫實現,而直接使用別人實現的哈希表比較多。對我們來說最重要的是要明白哈希表的原理、從而知道哈希表的特點,從而知道什么時候該用哈希表,當看到別人用了哈希表的時候要明白別人為什么要用哈希表、合適不合適?有沒有更好的選擇?
- 二叉樹、圖等。對于這些復雜數據結構,不要太當回事。這些復雜數據結構用到的概率很小(在嵌入式開發中),其實這些數據結構被發明出來就是為了解決特定問題的,你不處理特定問題根本用不到這些,沒必要去研究。
- 在實際應用中,實現數據結構和算法的人和使用數據結構和算法的人是分開的。實際中有一部分人的工作就是研究數據結構和算法,并且試圖用代碼來實現這些算法(表現為庫);其他做真正工作的人要做的就是理解、明白這些算法和數據結構的意義、優劣、特征,然后在合適的時候選擇合適的數據結構和算法來解決自己碰到的實際問題。
總結
- 上一篇: java嵌入浏览器_Java嵌入浏览器C
- 下一篇: Keil芯片安装包下载