計算機中的數與數據

前言

最近在看《Computer System: A Programmer's Perspective》,學會了很多基礎性的知識,於是總結出來與大家分享。

位與二進制

在現實生活中,我們會用紙和筆來記錄數據,比如在之前智能手機還沒有普及的年代,還有相當一部分人使用小本本來記錄電話號碼,顯然電話號碼作爲一種數據,記錄在紙上。

那麼在計算機中是如何記錄數據,表達信息的呢?

計算機使用一個序列的位(bit)來記錄數據。

一個位中存儲着一個二進制數字,什麼是二進制呢?二進制簡單來說就是逢二進位的一種進制,人類最常用,最直觀的一直使用着十進制,可用的符號爲:0,1,2,3,4,5,6,7,8,9,總共有10個符號,二進制則只需要兩個符號0和1。對機器來說,使用二進制是非常方便的一種形式,因爲二進制只需要兩種符號,也就是說,機器只需要能夠維持兩種不同的狀態來對應這兩種符號即可,比如高電壓和低電壓,通電和斷電等等。所以,對計算機來說,使用二進制(維持兩個狀態)是非常有效的方式。

計算機使用一連串的位來記錄數據,比如1位,那麼這個位上只能表示0或1,通常1位的數據幾乎沒有什麼作用。在現在的計算機中,使用8個位來作爲一個基本的單位,稱爲字節(byte),那麼它寫出來應該是這樣的:

//8個位都是0,對應十進制數字0,中間空開一個空格,四位四位的寫在一起是爲了可讀性
0000 0000   

//右側的一般稱爲最低位,左側的稱爲最高位,最低位加1,對應十進制中的1
0000 0001   

//最低位繼續加1,1+1=2,因爲是二進制,必須進位了,對應十進制數字2
0000 0010
0000 0011

...

//8位二進制最大值,對應十進制2^8-1=255
1111 1111 

以上就是一串從0開始遞增的二進制序列。

十六進制

剛剛說完了二進制數,現在簡單介紹一下十六進制數,二進制數與十六進制數之間有非常巧妙的關係

十進制需要10個符號:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
二進制需要2個符號:0, 1
十六進制需要16個符號:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f
其中,a-f分別對應着十進制中的10, 11, 12, 13, 14, 15

十六進制數值中的英文字母是不區分大小寫的

現在,讓我們來考慮4位二進制數

0000(2) -> 0(10) -> 0(16)   //括號中表示進制

一個4位的二進制數最小爲0000,也就是十進制中的0,也是十六進制的0。那麼4位二進制最大的值爲1111,那麼它的值爲


也就是說,四位二進制能表示數的範圍區間爲[0, 15],這剛好是1位16進制數所能表示的數值範圍。

四位二進制數能搞好使用一位十六進制數來表示

於是,當計算機中的數值使用二進制表示時,通常會出現成片成片的010101……,這看起來非常頭疼,非常容易讓人看錯,但是,我們可以從低位開始,四個四個的表示爲十六進制數。如下所示:

0000 0000(2) -> 00(16)
0000 1111(2) -> 0x0f    //通常,"0x"前綴用來指明是一個十六進制數
1111 0001(2) -> 0xF1 //十六進制的字母是不區分大小寫的,注意這裏大寫的F
 100 1111(2) -> 0x4f //注意!這裏的二進制數只有7位,通常我們從最低位開始轉換成十六進制數,高位在沒有指明的情況下,使用0補齊
0100 1111(2) -> 0x4f //上一行的二進制數高位補齊0的情況

有了十六進制,我們就可以方便簡潔的表示非常多位的二進制數,有了更高的可讀性。

整數

整數又分爲有符號與無符號的區別,無符號整數即是大於等於0的整數

無符號整數的表示

無符號整數的表示是基於最原始的二進制表示數據的辦法,也就是說,給定n位二進制序列,它所能表示的數值範圍是[0, 2^n - 1]

2^n-1是怎麼來的呢,其實很簡單,比如我們給出一個字節(8位)來表示一個整數,0000 0000,它能表示的最小值應該是0了,也就是8位全是0,那麼最大值呢?當然是8位全是1了,也就是1111 1111,現在不妨給它加個1,那麼它會變成9位的二進制數值1 0000 000,此時,這個9位的二進制數的值爲2^8 = 256,那麼8位最大值當然就是2^8-1=255了。

所以,無符號整數在計算機中的表示,就是簡單的對位序列的數值理解即可。

有符號整數的表示

通過位序列來表示有符號整數是非常巧妙的,稱之爲補碼編碼的方式來表示有符號整數。所謂使用補碼來表示有符號數,實際上對二進制序列並沒有什麼特別大的處理,它仍然是010101……這樣的一串東西,只是我們用一套稱爲“補碼”的規則來理解這串位序列而已。

補碼就是將位序列的最高位解釋爲負權。

比如,給定位序列,一個字節1000 0001,如果它是表示無符號整數,它的值是多少呢?很簡單1 \times 2^7 + 1 = 129,那麼如果我們用補碼來理解它呢?最高位是負權,也就是-1 \times 2^7 + 1 = -127

補碼會將最高位理解爲一個負數,直觀點來看,就是當最高位是1的時候,是一個非常大的負數加上後面的正整數,後面的正整數越大,這個負數越小,越靠近0,當最高位是0的時候,那麼負權整個都是0,剩下的幾位如同無符號整數一樣表示正整數。

我們用一串遞增的序列來理解。

0000 0000 -> -0 + 0 = 0
0000 0001 -> -0 + 1 = 1
...
0111 1110 -> -0 + 126 = 126
0111 1111 -> -0 + 127 = 127
1000 0000 -> -128 + 0 = -128
1000 0001 -> -128 + 1 = -127
1000 0010 -> -128 + 2 = -126
...
1111 1110 -> -128 + 126 = -2
1111 1111 -> -128 + 127 = -1

通過上述遞增的序列,你會發現,8位二進制能表示的有符號整數的範圍是[-128, 127],我們可以用數學的語言來描述一個n位的二進制能表示的有符號整數範圍是[-2^{n-1},2^{n-1}-1]

有符號數與無符號數的相互轉換

通常在高級程序語言中,有無符號整數的相互轉換是不改變位序列的,只是換了一種“解析”方式去解釋位序列,比如說1000 0000是一個無符號數,那麼它的值是128,如果我們把它轉換成一個有符號數,位序列不變,只是用補碼的方式去理解它,那麼它的數值就變成了-128了。

我來用一張圖片說明得更清楚一些。


如上圖所示,在數據大小爲8位的情況下,它的低7位所表示的數值範圍之間都是可以做到安全的轉換,但是當最高位不爲0的時候,有符號數與無符號數的相互轉換就會變成不安全的。

在我們寫代碼的時候一定要注意這一點。

擴展一個數值的位

如果我們要將一個8位的數據放到16位的容器中去,那麼我們需要對數據進行擴展, 也就是我們需要確定新的高8位應該是放0還是放1。

這個問題很簡單,對於無符號數的擴展,只要簡單的在高位上補充滿0即可,這種方式稱爲零擴展。對於有符號數,只需要在高位補充滿1即可,這種方式稱爲符號擴展

整數的加法計算

整數的計算分爲,無符號整數加法,有符號整數加法,無符號與有符號整數混合的加法。

對於無符號整數的計算,只是單單從位級上考慮就行了,但是當兩個二進制數相加後,最高位向前進1了,那麼就會產生溢出,本來應該變成一個更大的數,結果卻變小了。

對於有符號數的計算,同樣的也是從位級上進行加法計算,一樣的,最高位如果向前進一了,一樣會產生溢出。比如對於8位的數據,-128 - 128 -> 0,我們在位級上進行考慮

//這是一個豎式
1000 0000
1000 0000
----------
0000 0000

對於有符號數與無符號數混合的表達式,一般需要查看編譯器是如何處理這個問題的,有可能是將有符號數轉成無符號數再進行計算,也有可能是將無符號數轉成有符號數再進行計算。這個問題跟整型與浮點數相加是類似的問題,還是看編譯器/虛擬機是具體如何解決這個問題的。

浮點數

前面所說的有符號整數與無符號整數,都屬於定點數,就是小數點固定的數,而浮點數,即小數點是可以浮動(變化)的數。自從我聽到浮點數這個概念,在一個很長的時間裏,我都以爲浮點數就是指小數,其實不是這樣的,浮點數並不是狹隘的說一定要表示爲小數(如123.45),應該更準確的理解爲小數點的位置不是固定的,也就是說,這種數的“表示/解析”方法是可以表示小數點在不同位置的數的。

二進制小數

在解釋浮點數之前,我們先要知道一下二進制的小數是什麼情況。
先看一個二進制整數的例子,如101,那麼它的十進制數值爲1 \times 2^2 + 1 \times 2^0 = 5,那麼,當二進制數值有小數點時,101.101,它的十進制數值爲
1 \times 2^2 + 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2^{-3}= 4 + 1 + \frac{1}{2} + \frac{1}{8} = 5\frac{5}{8}

可以發現,0.1(2),0.01(2),0.001(2)……二進制小數每一位能表示爲\frac{1}{2}\frac{1}{4}\frac{1}{8}……因此它要表示某一個十進制的小數,需要用這些部分累加在一起實現。

IEEE浮點標準

在生產環境中,一個基本的浮點數都是32位的,不會像上文中一直使用的8位來當數據的大小,IEEE浮點標準中就規定了這32位應該如何使用。

它將位序列分爲三個部分來理解,
第一部分:符號,決定這個數是正數還是負數,一般用1表示負數,0表示正數。
第二部分:尾數,是一個二進制小數。
第三部分:階碼,是對浮點數的加權。

可以這樣去理解,有點像科學計數法,比如:
100,我們可以記爲0.1 \times 10^3
0.257,我們可以記爲0.257 \times 10^0
257,可以記爲0.257 \times 10^3

IEEE標準中,給定了32位與64位浮點數,各個部分的格式。
32位浮點數:
最高位:符號位(1位)-階碼(8位)-尾數(23位):最低位
64位浮點數:
最高位:符號位(1位)-階碼(11位)-尾數(52位):最低位

我們先假設一個8位的浮點數,並通過將它的數值列在一個表格中來觀察學習浮點數的標準是如何工作的。
8位浮點數,我們設定最高的1位是符號位,0表示正數,1表示負數,接下來的4位表示階碼最低3位表示尾數。請看下錶。

上表,只列出了符號位是0的情況。

規格化數與非規格化數

我們注意A列的描述,浮點數大體上分爲三種情況,非規格化數規格化數其他值

非規格化數的特徵是,階碼段的位都是0。
規格化數的特徵是,階碼段不全爲0,也不全爲1。
其他值的特徵就是,階碼段全爲1。

指數部分

然後,我們注意一下D列,有一個偏置值的概念,它的值是2^{k-1}-1k的值是階碼的長度(位寬),在我們自定義的8-bit浮點數中,k的值爲4,所以偏置值是7。

階碼E是分兩種情況的。
當這個浮點數是非規格化數的時候,E=1-偏置值
當這個浮點數是規格化數的時候,E=e-偏置值

e是階碼段的4-bit位序列所表示的無符號數。

所以,表格中E列指的是4-bit位序列按無符號數解析的十進制無符號數。

而F列,則是按是否是規格化數來決定的值。這個偏置值的設定與補碼負權的設計是非常相似的。

最終指數部分就是2^E了。

小數部分

小數部分M是由最後三位決定的,它同樣是分情況的。

當這個浮點數是非規格化數的時候,位序列BBB應當理解爲0.BBB,也就是整數部分爲0的二進制小數。
當這個浮點數是規格化數的時候,位序列BBB應當理解爲1.BBB,也就是整數部分爲1的二進制小數。

此時,對照表格的H列與I列,即可明白它們的含義。

浮點數的值

最終,這個浮點數的值就這樣得出來了value=sign \times 2^E \times M。sign是第一位決定符號的。

抽象數據模型(Abstract Data Models)

最近無意間看到一個這樣的概念,與計算機中的數有關,就在這裏提及下。

應用與操作系統都有一個抽象數據模型,大部分應用都沒有顯式的表現出這個模型,但是它會影響到代碼的編寫,在32 bits programming model(ILP32)上,integer, long, pointer都是32 bits的,大部分開發者都沒有意識到這一點。

現在系統擴展到64 bits,如果把所有的數據類型都擴展到64位是非常浪費的,因爲很多應用並不需要真的用到64位那麼大的數據格式,但是pointer卻需要擴展到64位,所以在LLP64/P64上,pointer被擴展到64位,其他的仍然保持32位。

以上內容翻譯自Abstract Data Models

抽象數據模型指定了編程語言中幾個基礎數據類型的大小。

比如LP64(可能是64-bit Leopard的縮寫)是使用在64位OSX系統或者Linux系統上的,它指定了integer爲32位,long是64位,pointer是64位。

還有LLP64,這是windows 64位操作系統所選擇的ADM,它的integer/long/pointer分別使用的是32/32/64位。

更細緻的說明與討論,我已經整理好了參考資料給大家。

參考資料

有看不懂的地方請給我說,我再添加更詳細的解釋;有講得不正確的地方還歡迎大家指正與討論:D

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章