起因
在代碼 review 的過程中,總是發現有人在數據類型轉換(reinterpret_cast)、大小端等問題(什么情況下需要考慮大小端,什么情況下不需要考慮)上犯錯誤,究其原因是沒有徹徹底底地搞懂數據的二進制表示。我想寫篇文章,用通俗易懂的語言把這件事情說明白,通俗易懂到我的女朋友也能看懂。於是我就嘗試着先做些鋪墊,給她講了些基礎。發現效果出奇的好,於是趕緊把這一過程記錄如下。
0 和 1 的世界
計算機的世界只有 0 和 1,所有的數據都由 0 和 1 的組合:數字、字母、漢字、圖片、音樂、電影、游戲、網頁等都可以由很多的 0 和 1 組成。
計算機如何知道一長串的 0 和 1 是什么含義呢?
比如 0100 0001
可能表示數字 65,可能表示大寫字母A
,可能和更多的 0 和 1 共同組成一個漢字,也可能表示圖片上某個點的顏色,其意義完全取決於人們約定的規則。
比特和字節
正着說:每一個 0 和 1 叫做一個比特(bit),8 個比特組成一個字節(Byte)。字節是計算機的基本單位,通常計算機一次最少處理一個字節。
例如:人們常說的一個 Word 文檔 100 KB,一張圖片 2 MB,一首歌 10 MB,一部電影 4 GB,內存 8 GB,硬盤 512 GB 等等。這里的大“B”就是 Bytes,字節。
比特(bit)最常見於寬帶的宣傳:例如 500M 寬帶的完整單位是 500 Mbps(注意這里是小“b”,不是大“B”)。bps 即 bits per second,500Mbps 指的是每秒最大傳輸 500 兆比特(bit)。所以 500M 的寬帶最快下載速度不是 500 MB/s,而是 500/8 = 62.5 MB/s。
反着再說一次:一個字節(byte)有 8 個比特(bit);每個比特只能是 0 或 1,8 個比特一共有 2^8 = 256 種組合,可以代表 256 種含義(具體含義完全取決於人們約定的規則)。
如何用 0 和 1 表示數字?
假設現在我們想用一個字節表示數字,於是我們可以約定,8 個 bit 低位到高位每個 bit 分別具有不同的權重,分別代表 1,2,4,8,16,32,64,128。於是通過一個字節 8 個 bit 的各種組合,就能表示出 0 到 255 之間所有的數字了。
高位 -> 低位 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|
權重 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
舉例:0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:35 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
舉例:65 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:128 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
對於這種不考慮負數的情況,我們稱之為無符號數。
那如何表示一個負數(有符號數)?
有很多種方法,只要約定好一個規則即可。比如我們可以約定,最高位 bit7 代表符號位,0 代表正數,1 代表負數。於是一個字節,8 個 bit 可以表示 -127 到 127 的數字。注意其中 0 有兩種表示,+0 和 -0。
高位 -> 低位 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|
權重 | +/- | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
舉例:+0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:-0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:35 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
舉例:-65 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:127 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
舉例:-127 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
現實計算機世界的負數幾乎都是補碼表示。和無符號數的規則相比,差別僅在最高位的權重為負。於是一個字節,8 個 bit 可以表示 -128 到 127 的數字。其中 0 只有一種表示。
高位 -> 低位 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|
權重 | -128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
舉例:0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:35 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
舉例:65 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:-128 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:127 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
舉例:-127 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:-1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
先停一下
看到這里,如果問你,1000 0000
代表一個什么數字,你要怎么回答?千萬別急着回答,回答之前應該先問清楚,要按照什么規則去解析。比如這串 0/1 表示的是一個無符號數還是一個補碼表示的有符號數。
如何表示更大的數?
用多個字節表示。一個字節不夠就兩個,兩個不夠就四個、八個。用 2 個字節就能夠表示 0 到 65535 之間的無符號數,用 4 個字節就能表示 0 到 4294967295 的無符號數!
高位 -> 低位 | bit 15 | bit 14 | bit 13 | bit 12 | bit 11 | bit 10 | bit 9 | bit 8 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
權重 | 32768 | 16384 | 8192 | 4096 | 2048 | 1024 | 512 | 256 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
舉例:0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:65 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:255 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
舉例:10000 | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
舉例:40256 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:60666 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 |
有符號數(補碼)也是類似的,只不過最高位的權重為負。用 2 個字節就能夠表示 -32768 到 32767 之間的有符號數,用 4 個字節就能表示 -2147483648 到 2147483647 的有符號數!
直接使用上面的表格(二進制表示的 bit 15 到 bit 0 和上面一模一樣),但是現在按照補碼的規則進行解析(即最高位權重為負),於是得到的結果就不一樣了。
高位 -> 低位 | bit 15 | bit 14 | bit 13 | bit 12 | bit 11 | bit 10 | bit 9 | bit 8 | bit 7 | bit 6 | bit 5 | bit 4 | bit 3 | bit 2 | bit 1 | bit 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
權重 | -32768 | 16384 | 8192 | 4096 | 2048 | 1024 | 512 | 256 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
舉例:0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:65 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
舉例:255 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
舉例:-25280 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
舉例:-4870 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 |
十六進制:二進制的簡化表示法
二進制要用 8 個 0/1 表示一個 byte,太不方便,為簡化表示,十六進制用分別用一個 0-F 表示一個字節的前 4 位和后 4 位。一般還會加上前綴0x
,以提醒讀者后面是 16 進制表示法。
如何表示帶小數點的浮點數?
還是一樣,只要約定好一個規則就行。計算機界流行的浮點數規則是 IEEE 定義單精度浮點(4 字節表示)和雙精度浮點(8 字節表示)。浮點數的規則要稍微復雜一些,但也沒什么特別難理解的。只是本文針對計算機初學者,不會涉及所有細節,后面可能會單獨寫一篇文章介紹浮點數的表示。
如何表示字符?
假設現在我們要用一個字節表示一個字符,我們可以約定,0000 0001 代表 a,0000 0002 代表 b,以此類推。從 0000 0000 到 1111 1111 的 256 種組合中表示 a-z、A-Z,加上各種標點符號也是綽綽有余。現實計算機世界幾乎都心照不宣地采用 ASCII 規則來表示常見的英文字符、標點以及一些不顯示的控制字符等。ASCII 只用了 7 個 bit,其中字符 a-z、A-Z、0-9 的表示在數值上都是連續的。選取部分例子如下:
進制 | 十進制 | 十六進制 | 字符/縮寫 | 解釋 |
---|---|---|---|---|
00000000 | 0 | 0x00 | NUL (NULL) | 空字符 |
00001010 | 10 | 0x0A | LF/NL(Line Feed/New Line) | 換行鍵 |
00001101 | 13 | 0x0D | CR (Carriage Return) | 回車鍵 |
00100000 | 32 | 0x20 | (Space) | 空格 |
00100001 | 33 | 0x21 | ! | |
00101100 | 44 | 0x2C | , | |
00101110 | 46 | 0x2E | . | |
00110000 | 48 | 0x30 | 0 | |
00110001 | 49 | 0x31 | 1 | |
00110010 | 50 | 0x32 | 2 | |
01000000 | 64 | 0x40 | @ | |
01000001 | 65 | 0x41 | A | |
01000010 | 66 | 0x42 | B | |
01000011 | 67 | 0x43 | C | |
01011000 | 88 | 0x58 | X | |
01011001 | 89 | 0x59 | Y | |
01011010 | 90 | 0x5A | Z | |
01100001 | 97 | 0x61 | a | |
01100010 | 98 | 0x62 | b | |
01100011 | 99 | 0x63 | c | |
01111000 | 120 | 0x78 | x | |
01111001 | 121 | 0x79 | y | |
01111010 | 122 | 0x7A | z | |
01111111 | 127 | 0x7F | DEL (Delete) | 刪除 |
如何表示漢字?
一個字節一共就 256 種排列組合,就算每個組合代表一個漢字,也只能表示 256 個漢字,這顯然是不夠的。要想表示一個漢字,至少需要 2 個字節。這樣就有 2^16 = 65536 種排列組合,可以表示 65536 個漢字了,應對常見的漢字已經不成問題。GB2312 編碼就是用兩個字節給漢字編碼的。比如 0xB0A1 代表漢字“啊”,0xD7D3 代表漢字“子”。完整編碼規則這里不詳細展開。
如何表示韓文、日文、阿拉伯文等所有字符?
每個國家、地區都有自己的編碼方式。比如同樣的一串數字 1011 0000 1010 0001
即 0xB0A1
在 GB2312 編碼下代表漢字“啊”,而在某種日文編碼規則中則可能代表一個日文字符。如果一個日本程序員開發了一個軟件,在日文編碼的機器上可以正常顯示日文,但是如果拿到中文編碼的機器上就會顯示亂碼。為解決這一問題,推出了 Unicode 編碼,為全世界的每一個字符都分配了一個獨一無二的編碼,甚至每個 emoji 表情都有自己的編碼!現在只要所有的軟件開發人員都統一采用 Unicode 編碼方式,就再也不會出現亂碼了。Unicode 采用 4 字節編碼,可以表示 2^32 = 4294967295 個字符,足夠容納目前世界上所有已知的字符了。但是如果考慮到將這些字符信息保存下來,比如說保存到硬盤上,新的問題就來了:比如對於一個英文的純文本文件,如何之前按 ASCII 編碼,每個字符只要 1 個字節就夠了,但是現在 Unicode 用 4 個字節編碼,如果每個英文字符都用 4 個字節來存儲(UTF-32),那么文件的大小將變成原來的 4 倍!為了解決這一問題,人們發明了 UTF-8。注意區分 UTF-8 和 Unicode:Unicode 是給每一個字符分配一個編號,從 0 到 4294967295;UTF-8 並不是給字符重新分配編號,每個字符的編號還是 Unicode 中定義、分配的編號,UTF-8 只是想方設法讓 Unicode 在保存、傳輸的過程中減少所需的字節數的一種小技巧/規則,具體細節暫時不在這里展開,后期可能會專門寫一篇文章介紹 UTF-8 是如何減少 Unicode 編碼文件的體積。
總結
計算機的世界由 0/1 組成,數字、字母、圖片等等所有信息都由一串串的 0/1 表示。8 個比特組成一個字節,字節是計算機的基本單位。一個字節可以表示 2^8 = 256 種含義,如何解析完全取決於人們約定的規則,要想知道字節的含義,必須要知道解析的規則(數據類型)。如果一個字節不足以表示所有的范圍、可能性,就用多個字節表示。