1、程序運行為什么需要內存
1.1、計算機程序運行的目的
(1)程序的目的是為了去運行,程序運行是為了得到一定的結果。
(2)計算機程序 = 代碼 + 數據。計算機程序運行完得到一個結果,就是說
代碼 + 數據 (經過運行后) = 結果。
(3)從宏觀上來理解,代碼就是動作,就是加工數據的動作;數據就是數字,就是被代碼所加工的東西。
(4)那么可以得出結論:程序運行的目的不外乎2個:結果、過程
- 用函數來類比:函數的形參就是待加工的數據(函數內還需要一些臨時數據,就是局部變量),函數本體就是代碼,函數的返回值就是結果,函數體的執行過程就是過程。
- 返回值是void類型的就是更在意過程,不那么在意結果。
1.2、計算機程序的運行過程
????????計算機程序的運行過程,其實就是程序中很多個函數相繼運行的過程。程序是由很多個函數組成的,程序的本質就是函數,函數的本質是加工數據的動作。
1.3、馮諾依曼結構和哈佛結構
(1)概念
- 馮諾依曼結構是:數據和代碼放在一起。
- 哈佛結構是:數據和代碼分開存在。
- 什么是代碼:函數
- 什么是數據:全局變量、局部變量
(2)舉例說明
- 在S5PV210中運行的linux系統上,運行應用程序時:這時候所有的應用程序的代碼和數據都在DRAM,所以這種結構就是馮諾依曼結構;
- 在單片機中,我們把程序代碼燒寫到Flash(NorFlash)中,然后程序在Flash中原地運行,程序中所涉及到的數據(全局變量、局部變量)不能放在Flash中,必須放在RAM(SRAM)中。這種就叫哈佛結構。
(3)哈佛結構和馮諾伊曼結構是由硬件設計決定的,而不是由操作系統決定的。
1.4、動態內存DRAM和靜態內存SRAM
?? 略
1.5、總結
(1)內存是用來存儲可變數據的,數據在程序中表現為全局變量、局部變量等(在gcc中,其實常量也是存儲在內存中的)(大部分單片機中,常量是存儲在flash中的,也就是在代碼段)。
(2)程序中需要數據,數據的存儲需要內存。
1.6、如何管理內存
(1)對于計算機來說,內存容量越大則可能性越大(能干的事越多),所以大家都希望自己的電腦內存更大。我們寫程序時如何管理內存就成了很大的問題。如果管理不善,可能會造成程序運行消耗過多的內存,這樣遲早內存都被你這個程序吃光了,當沒有內存可用時程序就會崩潰。所以內存對程序來說是一種資源,所以管理內存對程序來說是一個重要技術和話題。
(2)有操作系統和無操作系統
- 操作系統掌握所有的硬件內存,因為內存很大,所以操作系統把內存分成1個1個的頁面(其實就是一塊,一般是4KB),然后以頁面為單位來管理。頁面內用更細小的方式來以字節為單位管理。操作系統內存管理的原理非常麻煩、非常復雜、非常不人性化。那么對我們這些使用操作系統的人來說,其實不需要了解這些細節。操作系統給我們提供了內存管理的一些接口,我們只需要用API即可管理內存。
- 在沒有操作系統(其實就是裸機程序)中,程序需要直接操作內存,編程者需要自己計算內存的使用和安排。如果編程者不小心把內存用錯了,錯誤結果需要自己承擔。
(3)不同的語言提供了不同的操作內存的接口
-
匯編語言:根本沒有任何內存管理,內存管理全靠程序員自己,匯編中操作內存時直接使用內存地址(譬如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#等語言。
2、位、字節、半字、字的概念和內存位寬
2.1、什么是內存
(1)硬件角度
- 內存實際上是電腦的一個配件(一般叫內存條)。根據不同的硬件實現原理還可以把內存分成SRAM和DRAM。
- DRAM又有好多代,譬如最早的SDRAM,后來的DDR1、DDR2·····、LPDDR(low power低功耗內存,一般用于手機這樣的產品)。
(2)邏輯角度
- 內存可以隨機訪問(隨機訪問的意思是只要給一個地址,就可以訪問這個內存地址)
- 可以讀寫(當然了邏輯上也可以限制其為只讀或者只寫)
- 內存在編程中天然是用來存放變量的
- ?從邏輯角度來講,內存實際上是由無限多個內存單元格組成的,每個單元格有一個固定的地址叫內存地址,這個內存地址和這個內存單元格唯一對應且永久綁定。
- 邏輯上來說,內存可以有無限大(因為數學上編號永遠可以增加,無盡頭)。
- 現實中實際的內存大小是有限制的,譬如32位的系統(32位系統指的是32位數據線,但是一般地址線也是32位,這個地址線32位決定了內存地址只能有32位二進制,所以邏輯上的大小為2的32次方)內存限制就最大4G。
2.2、位和字節
(1)內存單元的大小單位有4個:位(1bit) 字節(8bit) 半字? 字? 雙字
(2)在所有的計算機、所有的機器中(不管是32位系統還是64位系統),位永遠都是1bit,字節永遠都是8bit。
2.3、字、半字和雙字
(1)字、半字和雙字是數據存儲和處理的基本單位。
(2)字、半字、雙字這些單位具體有多少位是依賴于平臺的。32位數據總線的字就是32位,64位的數據總線的字就是64位。
(3)半字永遠是字的一半,雙字永遠是字的兩倍。
2.4、內存位寬
(1)內存位寬(Memory Bus Width)是指內存與處理器之間數據傳輸通道的寬度,通常以位(bit)為單位。它決定了每次數據傳輸時能夠同時傳輸的數據量。位寬越大,每次傳輸的數據量就越多,數據傳輸速度也就越快。
(2)內存芯片之間是可以并聯的,通過并聯后即使8位的內存芯片也可以做出來16位或32位的硬件內存。
3、內存編址與尋址、內存對齊
3.1、內存編址
(1)內存在邏輯上就是一個一個的格子,這些格子可以用來裝變量。每個格子有一個編號,這個編號就是內存地址,內存地址和格子的空間是一 一對應且永久綁定的。這就是內存的編址方法。
(2)在程序運行時,計算機中CPU實際只認識內存地址,而不關心這個地址所代表的空間在哪里,怎么分布這些實體問題。因為硬件設計保證了按照這個地址就一定能找到這個格子,所以說內存單元的2個概念:地址和空間是內存單元的兩個方面。
(3)內存編址是以字節為單位的
3.2、內存和數據類型的關系
(1)C語言中的基本數據類型有:
- char?
- short(半個int)
- int? ?
- ong(有時是一個int,有時是兩個int)?
- float?
- double?
(2)一般情況下,int 整形的位數和CPU本身的數據位寬是一樣的,譬如32位的CPU,int就是32位的。
(3)數據類型和內存的關系就在于:
- 數據類型是用來定義變量的,而這些變量需要存儲、運算在內存中。所以數據類型必須和內存相匹配才能獲得最好的性能,否則可能不工作或者效率低下。
- 在32位的系統中,數據總線是32位的,這樣的硬件配置天生適合處理32位的變量,讀寫效率最高。
- 在很多32位環境下,我們定義bool類型變量,實際只需要1個bit就夠了,但是可能會用int來實現bool (int BoolVar) 。這么做實際上浪費了31位的內存,但是好處是效率高。
- 實際編程時要以省內存為主還是要以運行效率為主?答案是不定的,看具體情況。很多年前內存很貴機器上內存都很少,那時候寫代碼以省內存為主。現在隨著半導體技術的發展內存變得很便宜了,現在的機器都是高配,不在乎省一點內存,而效率和用戶體驗變成了關鍵。所以現在寫程序大部分都是以效率為重。
(4)注意:int 類型的大小并不完全由硬件平臺的位數決定,而是由C語言標準和編譯器共同決定。在PC系統中,int 通常為32位,即使在64位系統上也是如此,這是為了保持跨平臺的兼容性。
3.3、內存對齊
(1)定義:內存對齊是指數據在內存中的起始地址必須是某個特定值(通常是數據類型大小的倍數)的整數倍。例如,對于一個32位的整數(int類型),它的地址通常應該是4字節對齊的,即地址值是4的倍數。這是因為在計算機硬件層面,內存是以塊的形式進行訪問的,當數據對齊時,CPU可以更高效地讀寫這些數據。
(2)對齊單位:對齊單位通常是數據類型大小或者某個特定的值。例如,在一些32位系統中,常見的對齊單位有1字節、2字節、4字節等。對于基本數據類型,像char(1字節)、short(2字節)、int(4字節)等,它們的對齊單位通常是其自身大小。而對于結構體等復雜數據類型,其對齊單位可能是其成員中最大對齊單位的倍數。
3.4、配置結構體單字節對齊
(1)C語言中,配置單字節對齊主要有兩種常用方法:使用#pragma pack
指令和使用__attribute__((packed))
屬性。
(2)在結構體定義之前使用#pragma pack(1)
,可以使得該結構體及其后的結構體(直到遇到新的#pragma pack
指令)都按照單字節對齊。
/** struct Example的成員將不會插入任何填充字節,緊密排列。* char成員占1字節,int成員緊跟其后占4字節,short成員再往后占2字節,整個結構體大小為7字節。*/
#pragma pack(1)
struct Example{char c;int i;
};
#pragma pack() // 恢復默認對齊方式
struct Another{double d;char ch;
};
(3)__attribute__((packed))
是GCC編譯器提供的一個屬性,用于指定結構體或聯合體成員之間不插入填充字節,實現單字節對齊。
struct Example{char c;int i;short s;
}__attribute__((packed));
(4)配置單字節對齊可能會提高程序的內存使用效率,減少內存浪費。但是,它也可能會降低內存訪問效率。因為現代CPU在訪問對齊的數據時速度更快,當數據單字節對齊時,可能會跨越多個內存塊,導致CPU需要進行多次訪問和數據拼接,從而降低程序的運行速度。
4、C語言如何讀寫內存
4.1、C語言對內存地址的封裝
4.1.1、變量與內存
(1)在C語言中,變量是對內存地址的一種封裝。例如:
- int a;? 這行代碼讓編譯器為我們申請了一個 int 類型的內存格子。這個內存格子在32位系統中的長度是4字節,它有一個確定的地址,但這個地址編譯器知道就行,我們無需關心。編譯器將符號 a 與這個內存格子綁定在一起。
- a = 5;? 編譯器會將值 5 存入與符號 a 綁定的內存格子中。
- a += 4;? 這等效于 a = a + 4; 。編譯器會先讀取 a 原來格子中的值,將其與 4 相加,然后將結果寫回 a 對應的內存格子。
4.1.2、數據類型的本質
(1)C語言中數據類型的本質含義是:它決定了內存格子的長度和解析方法。
- 數據類型規定了內存格子的長度。例如,對于內存地址 0x30000000 ,原本它只代表1個字節的長度。但當我們給它一個類型 int 時,它就有了長度 4 字節。這意味著從 0x30000000 開始的連續4個字節( 0x30000000、0x30000001、0x30000002、0x30000003 )構成了一個 int 類型的格子。
- 數據類型還決定了內存單元格子中二進制數的解析方法。以內存地址
0x30000000
為例,若視為int
類型,其對應的4字節二進制數按int解析;若視為float
類型,則這4字節二進制數按float解析。
(2)普通變量、指針變量和數組
(1)關于數據類型,類型都只是規定了內存格子的長度和解析方法而已。
- 普通類型:int、float等。
- 指針類型: int *、float * 等
- 數組:int a[10]; floatb[10]等。
(2)舉例
- int a;? ? ? ??? 長度4字節,變量a地址 &a 。
- int b[10];?? 長度40字節,數組首元素的地址 b / &b[0] ,數組的地址 &b 。
- int *p;???????? 長度4字節,變量p的地址&p 。
4.1.3、強制類型轉換
強制類型轉換會改變變量的類型,從而影響其存儲和解析方式。
例如:int iVar;
- int *p = (int *)iVart;??????? 這將iVar的二進制值 解析為int *類型,賦值給變量p。
- char cVar = (char)Var;? 這將iVar的二進制值 解析為char類型,賦值給變量cVar。
4.1.4、函數名與內存地址
在C語言中,函數是對一段代碼的封裝。函數名的實質是這一段代碼的首地址,因此函數名本質上也是一個內存地址。
4.2、內存管理之數據結構
(1)數據結構的意義:數據結構就是研究數據如何組織(在內存中排布),如何加工的學問。
(2)最簡單的數據結構:數組
- 為什么要有數組?因為程序中有好多個類型相同、意義相關的變量需要管理,這時候如果用單獨的變量來做程序看起來比較亂,用數組來管理會更好管理。
- 數組的優勢:數組比較簡單,訪問用下標,可以隨機訪問。
- 數組的缺陷
- 數組中所有元素類型必須相同;
- 數組大小必須定義時給出,而且一旦確定不能再改。
(3)結構體
- 結構體發明出來就是為了解決數組的缺陷之一:數組中所有元素類型必須相同
4.3、結構體內嵌指針實現面向對象
(1)面向過程與面向對象
- 總的來說:C語言是面向過程的,但是C語言寫出的linux系統是面向對象的。
- 非面向對象的語言,不一定不能實現面向對象的代碼。只是說用面向對象的語言來實現面向對象要更加簡單直觀。
- 用C++、Java等面向對象的語言來實現面向對象簡單一些,因為語言本身幫我們做了很多事情;但是用C來實現面向對象比較麻煩,看起來也不容易理解。
(2)面向對象的核心概念
- 類封裝了數據和操作數據的方法,它是一種抽象的數據類型,用于定義一組具有相同屬性和行為的對象的模板。是實現封裝、繼承和多態等面向對象特性的基礎。
- 對象是類的一個實例,是類的具體表現形式。它是根據類的定義創建出來的具體實體,具有類定義的屬性和方法,并且可以存儲具體的值。
- 對象的屬性是對象所具有的數據特征,用于描述對象的狀態。屬性通常是一些變量,存儲了對象的具體信息。
- 方法是類中定義的函數,用于實現對象的行為。方法可以操作對象的屬性,完成特定的功能。
(3)使用結構體就可以實現面向對象,面向對象中類包含屬性(變量)和方法(函數)。如下所示:
- 結構體中的函數指針:類似于class中的方法。
- 結構體中的普通變量:類似于class中的屬性。
struct s
{int age; // 普通變量void (*pFunc)(void); // 函數指針,指向 void func(void)這類的函數
};
(4)補充
- 函數指針定義:? 指向函數的返回值類型? ?(*函數指針名)(參數類型1,參數類型2......)
- 指向函數的指針叫做函數指針,函數名可以賦值給函數指針,函數名就是函數在內存中的起始地址。舉例:
#include "stdio.h" int add(int data1,int data2)
{int a = data1+data2; return a;
}int main(void)
{int (*pfun)(int,int) = NULL;int res = 0; pfun = add;// 通過指向函數指針的變量來調用函數res = (*pfun)(4,5);//res = add(4,5); printf("res = %d\n",res);return 0;
}
5、內存管理之棧
5.1、什么是棧
(1)棧是一種數據結構,C語言中使用棧來保存局部變量。
(2)棧是被發明出來管理內存的。
(3)棧管理內存的特點(小內存、自動化)
(4)棧和隊列的對比
- 棧的特點是入口即出口,只有一個口,另一個口是堵死的,所以先進去的必須后出來。
- 隊列的特點是入口和出口都有,必須從入口進去,從出口出來,所以先進去的必須先出來,否則就堵住后面的。
- 棧:先進后出 FILO? ? first in last out?
- 隊列:先進先出 FIFO?? first in first out
5.2、棧的應用舉例:局部變量。
(1)C語言中的局部變量是用棧來實現的。
(2)我們在C中定義一個局部變量時,編譯器會在棧中分配一段空間給這個局部變量用。分配時棧頂指針會移動給出空間,給局部變量用的意思就是,將棧內存的內存地址和我們定義的變量關聯起來,對應棧的操作是入棧。
注意:這里棧指針的移動和內存分配是自動的(棧自己完成,不用我們寫代碼去操作)。
(3)函數退出的時候,局部變量要死亡,對應棧的操作是出棧。出棧時也是棧頂指針移動將棧空間中與變量關聯的空間釋放。這個動作也是自動的,也不用人寫代碼干預。
(4)棧的優點:棧管理內存,好處是方便,分配和最后回收都不用程序員操心,C語言自動完成。
(5)分析:C語言中,定義局部變量時如果未初始化,則值是隨機的,為什么?
??????? 定義局部變量,其實就是在棧中通過移動棧指針來給變量提供一個內存空間和這個局部變量名綁定。因為這段內存空間在棧上,而棧內存是反復使用的(臟的,上次用完沒清零的),所以說使用棧來實現的局部變量定義時如果不顯式初始化,值就是臟的。
5.3、棧的約束
(1)棧是有大小的,要避免棧的溢出,所以我們在C語言中定義局部變量時不能定義太多或者太大。譬如不能定義局部變量時 int a[10000]; 使用遞歸來解決問題時一定要注意遞歸收斂。
(2)相關補充
- C語言中,全局變量和靜態變量都是存儲在靜態存儲區的,他們在分配的時候都被系統默認初始化為0;而局部變量是在棧上分配內存的,如果不對它們進行初始化,那么他們可能是任意的隨機值。?
- 靜態存儲區(Static Storage Area)是程序運行時分配給靜態變量和全局變量的內存區域。這些變量在程序啟動時被分配內存,并在程序結束時釋放內存。與棧區和堆區不同,靜態存儲區的內容在程序的整個生命周期內都保持存在。
6、內存管理之堆
(1)堆內存的定義和管理
- 堆內存是程序運行時用于動態分配內存的區域。與棧內存不同,堆內存的分配和釋放不是由程序的執行流程自動完成的,而是由程序員通過特定的函數或方法手動控制。
- 在C語言中,使用
malloc
、free
進行內存分配和釋放的過程,通常被稱為堆管理。這是因為這些操作涉及到程序的堆內存,而不是棧內存。 - 堆(heap)是一種內存管理方式。堆內存是操作系統劃歸給堆管理器(通常是標準C庫的一部分)來管理的,然后向使用者提供API(如
malloc
和free
)來使用堆內存。
(2)堆內存的使用場景
- 大型數據結構:例如動態數組、鏈表、樹等。這些數據結構的大小通常在運行時確定,因此需要使用堆內存來動態分配空間。
- 全局數據:如果程序需要在多個函數之間共享數據,堆內存是一個很好的選擇,因為它可以被多個函數訪問。
(3)內存管理的復雜性
????????內存管理對操作系統來說是一件非常復雜的事情,因為首先內存容量很大,其次內存需求在時間和大小塊上沒有規律(操作系統上運行著的幾十、幾百、幾千個進程隨時都會申請或者釋放內存,申請或者釋放的內存塊大小隨意)。
- 內存碎片化:頻繁的分配和釋放可能導致內存碎片化,使得可用內存塊變得零散,影響內存的利用率。
- 內存分配算法:操作系統和堆管理器通常會使用復雜的內存分配算法(如伙伴系統、SLAB分配器等)來優化內存分配的效率和減少碎片化。
- 多進程和多線程:在多進程和多線程環境中,內存管理需要確保線程安全和并發控制,以避免競爭條件和數據不一致。
(4)注意事項
- 堆內存的限制:雖然堆內存比棧內存靈活,但并不是無限的。如果程序過度使用堆內存,可能會導致系統資源耗盡,甚至引發內存不足的錯誤(如
malloc
返回NULL
)。 - 內存泄漏和錯誤:使用堆內存時,程序員需要特別小心管理內存的分配和釋放。常見的問題包括內存泄漏(忘記釋放內存)、重復釋放(釋放同一塊內存多次)和野指針(使用已釋放的內存)。
(5)堆管理內存的優缺點
- 優點
- 靈活性:可以根據需要動態調整大小。
- 缺點
- 申請及釋放都需要手工進行,手工進行的含義就是需要程序員寫代碼明確進行申請malloc及釋放free。如果程序員申請內存并使用后未釋放,這段內存就丟失了(在堆管理器的記錄中,這段內存仍然屬于你這個進程,但是進程自己又以為這段內存已經不用了,再用的時候又會去申請新的內存塊,這就叫吃內存),稱為內存泄漏。在C/C++語言中,內存泄漏是最嚴重的程序bug,這也是別人認為Java/C#等語言比C/C++優秀的地方。
- 需要程序員去處理各種細節,所以容易出錯,嚴重依賴于程序員的水平。
(6)C語言操作堆內存的接口
- 堆內存釋放時最簡單,直接調用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中的可變大小的數組的方式。