這筆記整理起來還怪麻煩的
這只是第二章的一半。另一半看啥時候整理完吧
信息的表示和處理
三種最重要的數字表示:
-
無符號編碼
基於傳統的二進制表示法,表示大於或者等於0的數字
-
補碼編碼
表示有符號整數的最常見的方式,有符號整數就是可以為正或者為負的數字
-
浮點數編碼
表示實數的科學計數法的以2為基數的版本
整數的表示只能編碼一個相對較小的數值范圍,但這種表示是精確的
浮點數雖然可以編碼一個較大的范圍,但這種表示只是近似的
2.1 信息存儲
大多數計算機使用8位的塊,或者字節(byte)作為最小的可尋址的內存單位,而不是訪問內存中單獨的位
機器級程序將內存視為一個巨大的字節數組,稱為虛擬內存。內存中的每個字節都由一個唯一的數字來標識,稱為它的地址(address),所有可能地址的集合就稱為虛擬地址空間
十六進制表示法
一個字節由8位組成,在二進制表示法中,他的值域為0000000011111111,轉化為十進制則是0255 。但二進制太長,而十進制與位模式的互相轉化又很麻煩。替代方法便是用十六進制來表示位模式
16進制和2進制的互相轉化很好實現。如一個十六進制數字 0x173A4C,可以通過對每個十六進制數字進行二進制展開來轉化位二進制格式。轉化方法如下:

這樣就得到了二進制數字 000101110011101001001100
反過來也是一樣,只需將二進制數字每四位為一組轉化為十六進制數字即可。若二進制數字的位總數不是4的倍數,只需將最左邊的一組的前面用0補足即可
而要將十進制轉化為十六進制,則需要將十進制數字不斷用16整除,所得余數r為十六進制的最低位,商q則繼續用16整除。如數字314156的轉化:

則十六進制數字為0x4CB2C
反之則需要用相應的16的冪乘以每個十六進制數字。如數字0x7AF的轉化:
$$
7 * 16^2 + 10 * 16 + 15 * 16^0 = 7 * 256 + 160 + 15 = 1967
$$
字數據大小
每台計算機都有一個字長,指明指針數據的標稱大小。字長決定的最重要的系統參數就是虛擬地址空間的最大大小。對於一個字長為w位的機器而言,虛擬地址的范圍是0 ~ 2^w - 1,程序最多訪問2^w個字節
32位系統的限制虛擬地址空間位4千兆字節(4GB),64位系統則是16EB
計算機和編譯器支持多種不同方式編碼的數字格式,如不同長度的整數和浮點數。C語言支持的多種數據格式如下圖所示:

尋址和字節順序
對象在內存中排列字節的方法大致有兩種,分別為大端法和小端法。假設一個變量x的類型為int,地址位於0x100,十六進制值為0x01234567,則該變量在內存中的存儲如下圖所示:

大端法將最高有效字節排列在前面,而小端法則與之相反,將最高有效字節排列在后面
大多數Intel兼容機都只用小端模式,手機上的Android系統和IOS系統也只能運行小端模式,但實際上兩者並無明顯優劣
表示字符串
C語言中字符串被編碼為一個以null(其值為0)字符結尾的字符數組,每個字符都由某個標准編碼來表示,最常用的就是ASCII編碼
所有將ASCII碼作為字符碼的系統在編碼后的結果都會相同,而不受字節順序和字大小規則影響
表示代碼
考慮下面的C函數:
int sum(int x, int y)
{
return x + y;
}
這段代碼在不同系統編譯后產生的機器代碼如下:

可見不同機器類型擁有不同的編碼規則,因此二進制代碼是不兼容的,純粹的二進制代碼很難在不同機器與操作系統間移植。所以才會有將高級語言通過編譯器生成不同系統的原始語言的機制
布爾代數簡介
布爾運算大致有4種:
- ~ 對應 邏輯運算NOT(非),在命題邏輯中用﹁表示。當 p = 0 時, ~p = 1 ,反之亦然
- & 對應 邏輯運算AND(與),在命題邏輯中用∧表示。僅當 p = 1 且 q = 1 時,p&q = 1,否則 p&q = 0
- | 對應 邏輯運算OR(或),在命題邏輯中用∨表示。當 p = 1 或 q = 1 時,p|q = 1,否則 p|q = 0
- ^ 對應 邏輯運算 異或,在命題邏輯中用⊕表示。當 p = 1 且 q = 0 ,或者p = 0 且 q = 1 時,p^q = 1,否則 p^q = 0
假設 w=4 ,參數 a=[0110], b=[1100] ,則4種運算 a&b,a|b,a^b 和 ~b 的結果如下:

C語言中的位級運算,邏輯運算和位移運算
C語言支持按位布爾運算,也就是上面的&,|,^ 和 ~ 。這些運算能運用到任何“整形”數據類型上

C語言還提供了一組邏輯運算符,&&,|| 和 ! ,分別對應命題邏輯中的 AND,OR 和 NOT 運算
邏輯運算與位級運算不同,它將所有非零的參數視為TRUE,而參數0則視為FALSE

C語言還提供了一組移位運算。其中,左移會將最高的k位丟棄,並在右端補k個0
右移則分為兩種,一種是邏輯位移,即舍棄右端的地有效位值,直接在左端補k個0。算術位移則是在左端補上最高有效位的值

2.2 整數表示
整數編碼有兩種方式,一種只能表示非負數,也就是無符號數。另一種能夠表示負數,零和正數,也就是有符號數
兩者的區別主要在於有符號數需要將最高有效位用於記錄數字是否為負數,因此正數的表示范圍比無符號數要少一倍,但取而代之的是表達負數的能力
下圖是32位系統和64位系統中無符號數和有符號數的表示范圍:


整數編碼方式
整數的編碼方式主要有兩種,一種是無符號數的編碼,一種是補碼編碼(有符號數最常見的計算機表示方式)

光看公式可能有些難以理解,但加上實例就很好理解了

同理,補碼編碼也與之類似,不同的是補碼需要將最高位作為符號位來表示正負

其實只需要在計算過程中將最高位作為負數計算即可。最高位若為0則代表該數是正數,-0仍是0,對計算結果沒有影響。最高位若為1則代表該數是負數,乘以-1即可

此外還有一點,就是在之前的有符號數范圍中,負數的范圍基本都比正數大一。這正是因為有符號數的編碼表示中,以1開頭的負數占據了所有范圍的一半,相應的正數卻要拿出一位的范圍分給非負數0(即二進制編碼所有位數均為0的情況)
有符號數與無符號數之間的轉換
以一個16位的數字為例。若為有符號數,則該數字的表示范圍在 -32768 ~ 32767 之間,無符號數的范圍則是 0 ~ 65535
如此一來,若有一個負數(如 -12345)要轉化為無符號數,該如何轉化?
當然不是將負號去掉變成12345那么簡單。我們先將該有符號數用二進制表示出來:
-12345 = [1100111111000111]
該數轉化為無符號數后,最高位的符號位變為普通的數字位。原本要將該有符號數轉化為10進制,需要將最高位表示為: -1 * 2^15 = -32768
轉變為無符號數后,最高位則表示為:1 * 2^15 = 32768
相差 2^15 * 2 = 2^16 = 65536
也就是說,有符號數 -12345 轉化為無符號數的結果為 -12345 + 65536 = 53191
可以發現,兩者的差值正好是 2 的位數次方
相應的,一個超出有符號數正數范圍的無符號數轉化為有符號數,也會按照此規律進行轉換。如無符號數 53191 轉換為有符號數的結果為 -12345
大致轉換過程如下圖所示:

因此,為了避免類似這樣的轉換造成的溢出,部分高級語言(如java)拋棄了無符號數,只使用有符號數
不同類型整數的轉換
不只是有符號數和無符號數之間能夠互相轉換,不同類型的整數之間也能夠互相轉換。也就是下圖中所示的數據類型

先看低位整數轉換為高位整數。這里以16位的 short / unsigned short 轉換為32位的 int / unsigned 為例
無符號數的零擴展
若是無符號數的高位擴展,只需將無符號數左邊填上相應位數的零即可。二進制表示的 [01] 和 [0001] 都是一個數字
補碼數的符號擴展
若是有符號數就不能那么簡單了。畢竟負數需要保持最高位為1。因此有符號數只需將左邊填上相應位數的最高位即可。即正數在左邊填上相應位數的0,負數在左邊填上相應位數的1
下圖為執行轉換后的結果。其中 sx 為16位有符號數, usx 為16位無符號數, x 位32位有符號數, ux 為32位無符號數:

再看高位整數轉換為低位整數
int x = 53191;
short sx = (short)x; /* - 12345 */
int y = sx; /* - 12345 */
高位整數轉換為低位整數的方式是將左邊超出的位數直接截斷
要注意 short 是補碼類型,也就是說在轉換為 short 后產生了溢出,32位有符號數53191轉換為了16位的有符號負數 -12345
而有符號數的高位擴展方式為符號擴展,擴展為32位的 int 類型后仍是負數 -12345
