C語言之記憶體和位操作
記憶體和程式執行
程式執行的目的是為了得到特定的結果,計算機本質上是用於計算的,既然是用於計算,就需要參與計算的資料,那這些資料就儲存在記憶體中,計算之前參與運算的資料以及運算之後得到的資料,都儲存在記憶體中。
程式執行無外乎兩種目的,一種是為了得到某種結果,另外一種是為了執行某一種過程,在C語言中返回值void型別的函式就是為了執行某一種過程,有具體返回值的函式就是為了得到某種結果。
計算機程式的執行過程,就是很多函式相繼執行的過程,程式的本質就是函式,是由很多函式組成的,函式本質上是加工資料的動作。
哈佛和馮諾依曼結構
哈佛結構和馮諾依曼結構是CPU程式執行的兩種結構,分別表示資料和程式碼存放規則。程式碼指的就是操作資料的動作,也就是函式,而資料就指的是全域性變數以及函式中的區域性變數。
哈佛結構
資料和程式碼分開存放
馮諾依曼結構
資料和程式碼放在一起
動態記憶體和靜態記憶體
動態記憶體需要初始化之後才能使用,靜態記憶體不需要初始化就可以使用,讀寫速度較快。
程式的記憶體管理
記憶體用於儲存可變資料,在程式中資料就是程式的全域性變數和區域性變數,記憶體是程式的本質性需求,所以記憶體管理是寫程式中很重要的話題,堆積計算器來說記憶體容量越大,可操作任務就越多,所以理論來說記憶體越大越好,在寫程式時對記憶體的管理就成了大問題,如果管理不善可能造成程式消耗過多記憶體,當記憶體消耗殆盡程式就會崩潰,所以記憶體管理對程式來說是一個重要的技術和話題。
作業系統掌管所有的硬體記憶體,作業系統把記憶體分成一個一個記憶體塊,一般是4K的塊大小,也叫作頁,頁內用更細小的位元組單位來管理。作業系統管理記憶體的原理過於晦澀和難以理解,作業系統提供了記憶體管理的介面,我們只需要呼叫這些API即可管理這些記憶體。
在沒有作業系統的裸機程式上,程式需要直接操作記憶體,程式設計人員需要自己計算記憶體的使用和安排,所以如果不小心計算和使用錯誤,則程式就會變得很麻煩
不同的程式語言提供不同的記憶體操作介面,例如彙編中根本沒有任何記憶體管理,需要程式設計師自己直接使用記憶體地址來操作,C語言中編譯器管理直接的記憶體地址,程式設計師通過編譯器提供的變數名來訪問記憶體,如果需要大塊的記憶體可以通過API(malloc和free)來訪問記憶體,但是如果在裸機程式中需要使用大塊記憶體,需要定義基本的陣列來解決,在更高階的面向物件程式語言中,可以使用new關鍵字建立物件來分配記憶體,在Java語言中甚至有垃圾回收器回收不再使用的物件來自動釋放記憶體
位,位元組,半字,字以及記憶體位寬
從硬體來講,記憶體實際上是計算機上的一個配件,記憶體還可以分為SRAM和DRAM,DRAM又可以分為很多代,例如SDRAM,DDR,以及LPDDR等,從邏輯上說,記憶體可以隨機訪問,並且可以讀寫,由於記憶體可以隨機訪問,所以在程式設計中最適合用於存放變數,可以說有了記憶體C語言才可以定義變數,一般C語言中的一個變數,對應記憶體中的一個儲存單元
記憶體的程式設計模型
記憶體可以想象成一個個排列的單元格,每個單元格都有其特定的地址,且永久繫結,記憶體地址理論上可以有無限大,但是受制於CPU的位數,32位的CPU的記憶體地址範圍最大為2的32次方,也就是4G的記憶體,64位的CPU則可以有2的64次方的記憶體地址範圍
記憶體位寬
硬體記憶體的實現是有寬度限制的,有8位寬的記憶體,也有16位寬的記憶體,記憶體晶片之間可以並聯,並聯之後即使是8位的記憶體晶片,也可以做出16位或者32位的硬體記憶體,邏輯上,記憶體位寬是任意的,不論記憶體位寬是多少,對記憶體的操作不構成影響,但是由於操作需要硬體去執行,所以對記憶體的操作是受限於記憶體硬體的。
資料大小
- 位:指的是一個bit位
- 位元組:byte,8個bit的大小
- 字:一般是32bit
- 半字:一般是16bit
字和半字根據平臺是有所不同的,在程式設計時用到的較少。
記憶體編址和定址
記憶體地址的編排,和記憶體中的單元格相關,每個單元格都有一個唯一的編號,這個編號就是記憶體地址,該記憶體地址和單元格的空間是一一對應且永久繫結的,程式執行的時候CPU只認識記憶體地址,不會去關心這個地址對應的單元空間在哪裡,如何分佈等實際問題,因為硬體設計時就保證了根據該地址就可以找到該單元格,所以記憶體單元有地址和空間兩個重要概念,每個記憶體單元的地址和空間是對應的
記憶體編址是以位元組為單位的,也就是說,任意一個記憶體地址,都指的是位於該地址的記憶體單元格空間,比如0x00211100這個記憶體地址,代表的是位於硬體記憶體上的第0x00211100個這個單元格,它的容量是一個位元組,也就是8bit
記憶體和資料型別
C語言的基本資料型別,都和佔用的記憶體長度有相對應的關係
- int屬於整數型別,和CPU本身的資料位寬是一樣的,32位的CPU,一個int資料型別就是32bit,
- 資料型別用於定義變數,這些變數執行在記憶體中,所以資料型別必須和記憶體相匹配才能獲取最好的效能,否則可能不工作或者效率低下,例如在32位系統中使用int定義變數效率最高,因為此時的int本身就是32位的。
記憶體對齊
記憶體對齊是一個硬體問題,不同位寬記憶體硬體在邏輯上有相關性,例如32位的記憶體,則每4個位元組之間是具有邏輯相關性,記憶體訪問時如果跨邏輯相關空間來訪問,會額外的消耗資源,如果把訪問對齊,每次都按照邏輯相關空間的方式訪問,則效率會發揮的最好,非對齊的訪問會導致效率大打折扣。
C語言操作記憶體
C語言對記憶體的操作做了封裝,在C語言中使用變數名代表了某一塊記憶體地址,C語言中的資料型別,表示儲存該資料需要的記憶體單元格的長度,以及解析方式,C語言中的函式,就是一段程式碼的封裝,實質是這段程式碼的首地址,所以說,函式名本身就是一個記憶體地址。
使用指標來訪問記憶體,其實指標也是一種資料型別,所以說指標型別有自身要求的記憶體單元格長度和解析方式,使用指標來訪問記憶體,實際上是使用指標型別的解析方式來解析指標值開始的那段記憶體地址,解析的長度就是指標型別的長度,所以從本質來說,使用指標來訪問記憶體和用變數訪問記憶體是一樣的,都代表一段記憶體地址,只是對該記憶體地址的解析方法和解析長度不一樣而已。
使用陣列管理記憶體和使用變數使用記憶體其實本質上也是一樣的,只是對記憶體地址的解析方式和長度不一樣。解析陣列時,從陣列首元素的地址開始,按照陣列中的元素型別開始解析,連續解析陣列長度個元素,就得到了該陣列的全部資料。
記憶體和資料結構
資料結構本質上指的是資料在記憶體中如何排布,如何加工的問題。
陣列
陣列是最基本的資料結構,用於儲存一系列型別相同,意義相關的變數,陣列在記憶體中是連續排布的,佔用了一塊連續的記憶體空間。
陣列比較簡單,可以使用下標進行隨機訪問,但是陣列需要其中的元素的資料型別都是一樣的,並且在定義陣列時必須給出該陣列的大小,並且大小一旦確定之後就不能更改,沒有伸縮性。
結構體
結構體可以看成是一個複雜的陣列,目的是為了解決陣列中元素的資料型別必須一致的限制,在儲存一系列元素資料型別不一致的變數時,就必須使用結構體來儲存。
結構體內嵌指標
在結構體定義的時候,包含一些變數指標和函式指標,指向某個變數或者函式,則就可以實現出類,成員變數以及成員方法等面向物件的特性。
棧
棧是在C語言中的一種資料結構,用於管理記憶體,可以自動的幫助我們分配一些小塊記憶體,棧具有先入後出的特性,棧低指標始終指向棧的開始,棧頂指標可以進行上下移動,向棧中壓棧資料時棧頂指標向上移動一位
棧和區域性變數
在C語言中,區域性變數就是使用棧來實現的,在C中定義一個區域性變數,編譯器會在棧中尋找一塊記憶體,將該區域性變數壓入棧對應的記憶體中,這個動作是棧自動完成的,不需要寫程式碼來操作這一個過程,在函式退出的時候,棧將其中的元素從棧頂開始依次彈出,所有的區域性變數就自動銷燬了。
使用棧來管理記憶體,不需要額外寫程式碼來操作,記憶體分配和回收由C語言自動完成,在定義區域性變數時,由於變數所在的記憶體空間在棧中,棧裡邊的記憶體空間是進進出出反覆使用的,如果沒有進行初始化,則該區域性變數在棧中的記憶體空間可能存放的還是上一次彈出去的變數的值,所以這次新的變數的值就是一個隨機的未知數,這樣就是區域性變數未初始化值是隨機數的原因。
棧的約束
棧是有固定大小的,棧的大小不好設定,太小怕溢位,太大怕浪費,所以棧不夠靈活,在區域性變數定義過多或者過大時,或者遞迴層級過多時,就有可能造成棧溢位
堆
堆是記憶體的另外一種管理方式,堆管理沒存比較自由,可以隨時申請和釋放,記憶體的大小可以根據需要自定義,作業系統通常把記憶體劃分區塊,把一部分記憶體區域分配給堆來管理,堆記憶體並不屬於某一個程序,由堆管理器進行管理,程序呼叫堆管理器提供的記憶體管理API(malloc和free)來分配和釋放記憶體。
堆記憶體通常適用於記憶體容量較大,需要反覆使用和釋放的情況,很多資料結構也是使用堆來實現的。
堆記憶體特點
- 堆記憶體通常不限於容量
- 申請和釋放都需要程式設計師手工進行,在程式程式碼中呼叫malloc以及free函式進行記憶體分配和釋放,如果申請了記憶體沒有釋放,則該段記憶體丟失,會造成記憶體洩露
- 堆記憶體比較靈活,空間大小可以隨意控制,但是由於過於靈活,需要程式設計師處理的細節較多,所以容易出錯。
堆管理器介面
- free:堆記憶體釋放使用free函式,free(void *ptr)
- malloc:分配記憶體,單位為位元組,void *malloc(size_t size)
- calloc:批量分配記憶體,void *calloc(size_t nmemb,size_t size)
- realloc:重新分配記憶體,改變原來申請記憶體的空間大小,void *realloc(void *ptr,size_t size)
其他資料結構
除了一些基本的資料結構之外,還有一些其他的資料結構。
連結串列
連結串列使用之處比較多,在linux驅動和應用程式設計時中就常見到連結串列這種結構。
連結串列中有一個一個的節點,每個節點通常包含三部分:前指標,資料域和後指標,前指標指向該節點之前的節點,後指標指向該節點之後的節點,資料域表示當前節點的資料。
雜湊表
雜湊表和陣列類似,不同之處在於,雜湊表不使用數字作為索引,而是用該元素的雜湊值作為該元素的索引,元素查詢的時候根據雜湊值查詢該元素,每個元素和雜湊值是一一對應的對映關係,由於雜湊值的單向性,所以雜湊表中的索引,是不會出現重複的。
資料結構和演算法
現實生活中的問題是多種多樣的,問題的複雜度不同,解決問題使用的演算法和資料結構不同,所以產生了多種不同的資料結構,每個資料結構的發明都是為了配合特定的演算法,每個演算法是為了樹立特定的問題,演算法的實現,依賴於對應的資料結構
位操作
常用的位操作符號
常用的位操作符有大概6種
- 位與&:只有兩個位都為1時,結果為才1
- 位或 |:只要兩個位中有一個為1,結果就為1
- 取反 ~:對於特定位,如果位等於0,則結果為1,如果位等於1,則結果為0
- 異或 ^:兩個位同為1或者同為0時,結果為0,否則結果1
- 移位 <<或者>>:對於一個數,將其二進位制位向左統一向左邊移動n位,稱為左移<<,右邊空出來的位補0,統一向右邊移動n位,稱為右移>>,左邊空出來的位,無符號數補0,有符號數補符號位。
C語言中還有邏輯與(&&),邏輯或(||)或者邏輯取反(!),和位操作不同的是,位操作是把運算元按照二進位制位按位操作,邏輯操作是將運算元作為整體來操作。
操作暫存器
暫存器的操作適合使用位運算來執行,所以常用的暫存器操作都可以使用位操作來實現:
- 特定位清0,使用&操作,將該數的特定位和0相&,就可以把這些位置為0,
- 特定位置1,使用|操作,將該數的特定位和1相|,就可以把這些位置為1
- 特定位取反,使用^操作,將該數的特定位和1相^,就可以把這些位置為相反的值
構建特定二進位制數
為了給特定位設定特定的值,需要構造一個特定的二進位制數,這個二進位制數的特定位要符合要修改的數的位要求,在這種情況下,可以使用左移或者右移運算,來得到一個特定的二進位制數,例如要得到一個0000 0000 1111 0000的二進位制的數,我們可以先獲取一個0000 0000 0000 1111的二進位制數,也就是0xFF,然後將其進行左移4位,就得到了0xFF00,也就是0000 0000 1111 0000。
巨集定義實現位運算
在巨集定義中使用位運算,可以使用巨集定義來進行置為和復位,例如:
- #define SET_NTH_BIT(x,n) (x | ((1U) << (n - 1))),將變數x的n位置1
- #define CLR_NTH_BIT(x , n) (x & ~((1U) << (n - 1))),將變數x的n位清零
還可以擷取變數的部分連續位:
- #define GET_BITS(x,m,n,) ((x & ~(~(0U)<<(m - n + 1)<<(n - 1))>>(n - 1))),擷取變數x的從m開始到第n位截取出來