JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

用二進位與十六進位來數數

電腦裡的每一個值,說到底都只是一排開/關的開關。本篇從這些開關往上把「數數」這件事重新蓋一遍——二進位、它們聚成的位元組,以及每位系統程式設計師都能流暢閱讀的十六進位簡寫。

機器為什麼用二來數

在基礎那一級,我們把記憶體想成一條長長、編了號的信箱街,每個信箱裝著一個數。現在我們打開一個信箱,問一個再自然不過的問題:一個數究竟是怎麼存進去的?這個答案是本級其餘一切的基石。下到矽晶層面,電腦沒有「數字 7」這種概念,沒有「藍色」,也沒有「字母 A」。它有的只是開關——一個個或正通著電、或沒通電,開或關、高或低的微小電路。每一個這樣的開關就是一個位元,資訊最小的單位:一個單純的是或否。

一個開關只能說兩件事,所以要數到 1 以上,你就把好幾個開關排成一列,整列當成一個數來讀。這恰恰就是你早已在日常的十進位裡做的事,只是你從不去想它。在十進位裡,每一欄的值是它右邊那欄的十倍:305 這個數代表 3 個一百、0 個十、5 個一,因為各位的位值是十的次方。二進位玩的是一模一樣的遊戲,只改一處——每一欄的值是它右邊那欄的倍,因為每個開關只有兩種狀態而非十種。位值於是變成 1、2、4、8、16、32 等等:二的各次方。

所以讀一個二進位數,不過就是把所有開關為「開」的位值加總起來。拿樣式 1011 來看。從右邊起,各欄的值是 1、2、4、8。為「開」的位元落在 8、2、1 三欄,於是 1011 就是 8 + 2 + 1 = 11(十進位)。這就是二進位的全部把戲:一排開關、位值往左每欄加倍、把「開」的加起來。沒有任何隱藏;它就是在一具二進位算盤上做尋常的數數。

binary:    1    0    1    1
place:     8    4    2    1
value:     8  + 0  + 2  + 1   =  11 (decimal)

0011 0010  ->  32 + 16 + 2  =  50
讀一個二進位樣式:在每一欄上方寫下位值,再把「開」的加起來。第二列把八個位元分組,中間留個空隙以便閱讀。

位元聚成位元組

單獨一個位元太小、派不上用場,所以機器把它們綑成一束。勝出的那一束是八個位元,我們叫它一個位元組——記憶體就是以它為單位來定址、來販售的。八個開關給出 2^8 = 256 種相異樣式,從 0000 0000 一路到 1111 1111,這表示一個位元組能表示 256 個不同值之一:若當成無號整數來讀,就是 0 到 255。這個 0 到 255 的範圍值得記下來;老派的色彩通道或一個 `char` 之所以常常封頂在 255,原因就在這裡。

半個位元組——四個位元——出現得太頻繁,於是有了自己的名字,叫做 半位元組(沒錯,nibble 就是「小口咬」的意思)。一個半位元組裝得下 2^4 = 16 種樣式,也就是 0 到 15 的值。把這個 16 牢牢記住:它正是把二進位和我們即將見到的十六進位接起來的樞紐。更大的束也有你會不斷用到的名字。某顆中央處理器偏好一次處理的天然區塊——在你會寫程式的機器上通常是 4 或 8 個位元組——就是它的機器字。一台 64 位元機器有一個 64 位元(8 位元組)的機器字,這也是為什麼一個普通的 `int` 或一個指標通常就是那個大小。

十六進位:人真的讀得懂的二進位

二進位很誠實,但讀起來累人:單單一個位元組就是八個數字,而一個 32 位元的值是一面三十二個 1 和 0 堆成的牆,你的眼睛在上頭一滑就掉了。所以系統圈的人幾乎從不徒手寫赤裸的二進位。他們改寫十六進位——以十六為底——而它之所以如此漂亮地管用,是因為一個半位元組(四個位元)恰好對應到一個十六進位數字。十六種樣式,十六個數字。普通數字到 9 就用完了,於是十六進位借用字母 a 到 f 來表示 10 到 15 的值:a 是 10、b 是 11、c 是 12、d 是 13、e 是 14、f 是 15。

為了不讓十六進位被誤認成普通的數,C 和多數工具都在它前面加上 0x。所以 0x1f 是個十六進位值,而光寫 1f 則毫無意義。在這些指南中,我們一律用小寫加這個前綴來寫十六進位,像是 0x1f 或 0x7fffffff。一旦看懂半位元組的把戲,把一個位元組轉成十六進位根本不用任何算術:把八個位元拆成兩個半位元組、各自變成單一的十六進位數字,就完成了。位元組 0011 0010 拆成 0011(是 3)和 0010(是 2),得到 0x32。反過來讀回去同樣輕鬆:0x32 就是半位元組 3 和 2,也就是 0011 0010。

  1. 把位元組的八個位元,從正中間拆成兩個各四位元的半位元組。
  2. 把每個半位元組當成 0 到 15 的數來讀,方法是把它的位值(8、4、2、1)加起來。
  3. 把每個值換成它的十六進位數字:0 到 9 維持原樣,10 到 15 變成 a 到 f。
  4. 把兩個十六進位數字並排寫下、加上 0x 前綴——那就是你的位元組,例如 1110 1011 變成 0xeb。

現在好處兌現了。因為兩個十六進位數字恰好涵蓋一個位元組,十六進位便成了談論記憶體的天然語言,你會到處看見它:位址、遮罩、以及赤裸的位元組傾印。一個位元組永遠是兩個十六進位數字,所以一個 32 位元(4 位元組)的值永遠是八個——這也是為什麼像 0x7fffffff 這樣的值,一眼就認得出是四個位元組寬。當除錯器把某個位址顯示成 `0x4011a0`,或本級稍後的指南寫出一段進位之間的轉換時,那份精簡正是十六進位、而非二進位,成為實務記法的緣由。

徒手在各進位之間轉換

你很少用算術把二進位轉成十六進位,因為半位元組的分組是瞬間的事。但與十進位——人腦思考所用的進位——之間的來回轉換,確實需要一個小方法,而且值得親手做幾次,好讓神祕感消散。要把一個十進位數轉成二進位,就反覆地把它除以二、記下餘數。要反過來走,就把位值乘開來相加,正如我們稍早對 1011 所做的那樣。一旦見過,兩個方向都是機械化的。

  1. 從你的十進位數開始,比方說 50,除以 2,記下商和餘數:50 = 25 餘 0。
  2. 再除一次商:25 = 12 餘 1,接著 12 = 6 餘 0、6 = 3 餘 0、3 = 1 餘 1、1 = 0 餘 1。當商變成 0 時停下。
  3. 把餘數從最後一個讀到第一個——由下往上——得出二進位數字:110010。
  4. 若你願意,往左補成完整一個位元組(0011 0010),分成半位元組以讀出十六進位:0x32。用位值相加來驗算:32 + 16 + 2 = 50。

那個實作範例把十進位 50 一路走到 0x32 再走回來,而這趟來回正是最好的證明:這並非三個不同的數,而是同一個值披著三套戲服。桌上五十樣東西就是五十樣東西,無論你把這堆標成 50、0011 0010、還是 0x32。進位只是記法;底下的數量是同一個。把這件事內化——一個數和它的書寫形式是兩回事——就掌握了本級所教的一半,而這也是接下來幾篇指南用以蓋起有號數與浮點數的地基。

這把你帶到了哪裡

退一步,看看你實際上得到了什麼,因為這不只是個客廳把戲。你現在能用三種方式看一個位元組——當成八個開關、當成一個十進位計數、當成兩個十六進位數字——並隨意在它們之間滑動。這份流暢,正是整個資料表示法那一級賴以站立的識讀能力。本級其餘每一篇指南,骨子裡都是在你現在讀得懂的同一排排位元之上,鋪上一種新的解讀

這裡留個誠實的懸念。上面所有內容都悄悄假設了我們的數永遠不是負的——只有 0 與往上。但真實的程式會做減法,而 `5 - 8` 理當是 -3。當你手上只有開/關的開關、沒有任何欄位能放一個符號時,你要怎麼存一個負號?這有個確實巧妙的答案,二的補數,而把它拆開來看正是第 2 篇指南的全部工作。你已學會閱讀的這些位元,即將學會第二種被閱讀的方式。