計算機中數值和字符串怎么用二進制表示?
大家好,我是呼嚕嚕。我們都知道現代計算機采用 0 和 1 組成的二進制,來表示所有的信息。那大家是不是有時候會有這些疑問:為什么計算機采用了二進制?二進制是如何表示計算機的相關信息的?比如數字、字符串、聲音、圖片、視頻等等
進制
進位計算法是一種常見的計算方式,常見的有十進制,二進制,十六進制
- 十進制
十進制,都是以0-9這九個數字組成,不能以0開頭, 逢十進一。十進制是我們從小就潛移默化般學習的,我們大多數人擁有的手指或腳趾的數目就是10,天生讓我們適合十進制為基礎的數字系統
- 二進制
二進制,數字中只有 0 和 1,逢二進一
- 八進制
八進制,數字0-7,逢八進一
- 十六進制
十六進制,數字有 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F組成,逢十六進一。其表示形式比較特殊,因為10~15不能用數字來展示,所以強制規定:10 用 A 表示、11 用 B 表示、12 用 C 表示、13 用 D 表示、14 用 E 表示、15用F表示
進制間的轉換
- R進制 → 十進制:按權展開
- 十進制 → R進制:整數小數分開處理
- 整數部分的轉換方法是:“除基取余,上右下左”。即用要轉換的十進制整數去除以基數R,將得到的余數作為結果數據中各位的數字,直到余數為0為止。上面的余數(先得到的余數) 作為右邊低位上的數位,下面的余數作為左邊高位上的數位。
- 小數部分的轉換方法是:“乘基取整,上左下右”。即用要轉換的十進制小數去乘以基數R,將得到的乘積的整數部分作為結果數據中各位的數字,小數部分繼續與基數R相乘。以此類推,直到某步乘積的小數部分為0或已得到希望的位數為止。最后,將上面的整數部分作為左邊高位上的數位,下面的整數部分作為右邊低位上的數位。
我們需要注意的是:在轉換過程中,可能乘積的小數部分總得不到0,即轉換得到希望的位數后還有余數,這種情況下得到的是近似值。
- 二進制轉八進制、十六進制
由于把二進制的三位看成一個整體就是八進制的數,二進制的四位也就是十六進制的數。通過這個規律,我們很容易地就能實現二進制與八進制、十六進制的相互轉換。整數部分從低向高每3或4位數用一個等值八/十六進制數替換,不足時高位補0;小數部分從高向低每3或4位數用一個等值八或十六進制數替換,不足時低位補0
- 八進制、十六進制 轉二進制
每一位數改成等值的3或4位二進制數,整數部分高位0省略;小數部分低位0省略
計算機為什么使用二進制?
我們從小更熟悉十進制的運算,0、1、2、3、4、5、6、7、8、9十個數字,逢十進一。但是計算機中使用二進制,只有0和1兩個數字,逢二進一。
采用二進制的原因:
- 二進制在自然界中最容易被表現出來。自然界中二值系統非常多,電壓的高低、水位的高低、門的開關、電流的有無等等都可以組成二值系統。
- 計算機使用二進制和現代計算機系統的硬件實現有關。制造二個穩定態的物理器件容易,使得組成計算機系統的邏輯電路通常只有兩個狀態,即開關的接通與斷開。由于每位數據只有斷開與接通兩種狀態,因此二進制的數據表達具有抗干擾能力強、可靠性高的優點
- 二進制非常適合邏輯運算,可方便地用邏輯電路實現 算術運算
機器數和真值
機器數
一個數在計算機中的二進制表示形式, 叫做這個數的機器數、機器碼。由于我們平時不僅使用的是正數,還有大量的負數,而計算機是無法識別符號"+","-", 所以計算機規定,用二進制數的最高位0表示正數,如果是1則表示負數。機器數是帶符號的
如果十進制中的數 +3 ?,計算機字長為8位,轉換成二進制的機器數就是0000 0011?。如果是-3?,就是 10000011
真值
帶符號位的機器數對應的真正數值是 機器數的真值,我們知道機器數的第一位是符號位, 比如1000 0011?直接轉換成十進制為131?,但實際上最高位1 是負號,其真正的值為 [-3]
機器數的編碼形式有哪些?
原碼
原碼就是符號位加上真值的絕對值, 即用最高位表示符號, 其余位表示值
比如如果是8位二進制:
- [+1] = (0000 0001)原
- [ -1] = (1000 0001)原
我們人類根據二進制的規則,可以一眼就明白原碼代表的數字,方便了人類
面試的時候有一個經典的問題:8位二進制數原碼的取值范圍是?我們只需將除了最高位,用來表示符號,其他位都是1,即[1111 1111 , 0111 1111],換算成十進制:[-127 , 127]
那n位二進制數呢?
取值范圍:
image-20221129211701296
現在看起來都是那么美好,然而當我們將正負數相加時,遇到了問題:2個[+1]?相減 ,其實就相當于[+1]? 和 [-1]? 相加,我們的預期是0?, 但計算機實際上計算時:(0000 0001)原+(1000 0001)原=(1000 0010)原 =[-2]
為了解決這個問題,反碼就應運而生了
反碼
反碼主要是針對負數的,正數的反碼是其本身,負數的反碼是在其原碼的基礎上, 符號位不變,其余各個位取反
- [+1] = (0000 0001)原 = (0000 0001)反
- [-1] = (10000001)原 = (1111 1110)反
反碼如果是表示的一個正數,那我們還是一眼就能知道他的數值,但如果是負數的反碼時,我們就需要轉換成原碼才能看出它的真值。如果最高位有進位出現,則要把它送回到最低位去相加(循環進位)的
- [- 1] = (1000 0001)原 = (1111 1110)反
- [+7] = (0000 0111)原 =(0000 0111)反
- [-1] + [+7] = (1111 1110)反 +(0000 0111)反= (1 0000 0101)反 = (0000 0110)反 =[+ 6]
2個正數相減:[+1] - [+1] = [+1] + [-1] = (0000 0001)反 + (1111 1110)反 = (1111 1111)反 =(1000 0000)原 = [-0]這樣就完美實現了“正負相加等于0",但奇怪的是 ,這個[-0]是有符號的,這就要歸因于 原碼的設計之初,存在的問題,
- (1000 0000)原=[- 0]
- (0000 0000)原=[+0]
對的,你沒看錯,零竟然有2個,習慣計算機的萬事萬物一一對應,嚴謹認真的工程師們表示無法接受,得想辦法去掉[-0],最后他們就發現了神奇的補碼
補碼
補碼的規則:針對負數繼續改進了思路:正數的補碼就是其本身。負數的補碼是在其原碼的基礎上, 符號位不變, 其余各位取反, 最后一位+1。即在反碼的基礎上最后一位+1
[+1] = (0000 0001)原 = (0000 0001)反 = (0000 0001)補
[-1] = (1000 0001)原 = (1111 1110)反 = (1111 1111)補
[+1] - [+1] = [+1] + [-1] = (0000 0001)補 + (1111 1111)補 = (1 0000 0000)補 = (0000 0000)補 = [0]如果補碼在補一位1的時候,發生最高位進位,會自動丟掉最高位。期間引用了計算機對符號位的自動處理,利用了最高位進位的自動丟棄實現了符號的自然處理。
那(1000 0000)補? 那現在表示多少?-128
- (1000 0000)補 =-1 * 2^7 =[-128]
- (1011)補 = -1 * 2^3 + 02^2 + 12^1 + 1*2^0 = -5
- (0011)補 = 0 * 2^3 + 02^2 + 12^1 + 1*2^0 =3
如果是8位二進制, 使用原碼或反碼表示的范圍為[-127, +127], 而使用補碼表示的范圍為[-128, 127],使用補碼還能夠多表示一個最低數
補碼其實脫胎于 模運算系統:比如一天中的24小時是一個模運算系統,任意時刻的鐘點數都是0到23間的一個整數,這有點類似24進制
- 今天的第24點,就是明天的0點;
- 今天的25點,就是明天的凌晨1點;
- 今天的-4點,就是昨天的20點,我們稱20是-4對模24的補碼,模就是容量、極值的意思
再舉個例子:鐘表上的12個刻度也是一個模運算系統。假定時鐘現在指向10,要把指針只向6,有兩種方法
- 倒撥4格:10 - 4 = 6
- 正撥8格:10 + 8 = 18 = 6 (mod 12)
所以模12系統中 -4 = 8 (mod 12),我們稱8是-4對模12的補碼
一個模運算系統中:一個負數可以用它的正補數(負數的補碼)代替,一個負數的補碼 = 模 - 該負數的絕對值
那我們之前公式 一個負數的補碼 = 符號位不變, 其余各位取反, 最后一位+1,是怎么來的?
負數的原碼 取反 再加1, 這只是方便大家記憶的手段,實際上它相當于加一個模256也就是2^8,為什么要拆,這是由于8位機,8位2進制數,至能表示0~255個數,一共256個數,所以它是表示不了256這個值的,只能是255+1。由于計算機系統里面不僅只有正數,還有負數呢,這個該怎么表示?計算機大師就想到了,可以將256個數一分為二,規定最高位為符號位,最高位1開頭的表示為負數,最高位0開頭表示正數。我們這里需要注意一下,特殊的0,所以8位2進制數表示范圍就變成了[-128,127],這個范圍是不是很熟悉!
[-1] = (1000 0001)原 = (1111 1110)反 = (1111 1111)補?,如果符號位參與計算,(1111 1111)補 的十進制 等于 255 。而255 + |-1|= 256?,也就是模。補碼本天成,妙手偶得之
小結一下:
- 補碼不僅解決了[-0]的問題,更核心的是讓計算機做減法運算,變成加法運算。A - B = A + B的補碼
- 使用補碼,將減法變成加法運算,這樣硬件上只需有加法器即可,不需要其他硬件,降低了電路的復雜度
- 使用補碼,不浪費編碼個數,存儲空間利用率高
- 補碼可以用n&0判斷負數奇偶
- 所以計算機底層存儲數據時使用的是二進制數字,但是計算機在存儲一個數字時并不是直接存儲該數字對應的二進制數字,而是存儲該數字對應二進制數字的補碼
定點數和浮點數
定點數
定點數的意思是:即約定機器中所有數據的小數點位置是固定不變的。通常將定點數據表示成純小數或純整數,為了將數表示成純小數,通常把小數點固定在數值部分的最高位之前;而為了將數表示成純整數,則把小數點固定在數值部分的最后面。例如:十進制的 25.125
- 整數部分:25使用二進制表示為:11001
- 小數部分:0.125使用二進制表示為:.001
- 所以合起來使用11001.001 表示十進制的25.125
本文的原碼、反碼、補碼概念都是基于定點數
浮點數
定點數表示法的缺點在于其形式過于僵硬,固定的小數點位置決定了固定位數的整數部分和小數部分,不利于同時表達特別大或特別小的數,最終,絕大多數現代的計算機系統采納了浮點數表達方式,這種表達方式利用科學計數法來表達實數,即用一個尾數(Mantissa,尾數有時也稱為有效數字,它實際上是有效數字的非正式說法),一個基數(Base),一個指數(Exponent)以及一個表示正負的符號來表達實數 例如:
- 352.47 = 3.5247 * 10的2次方
- 178.125轉化為二進制為 10110010.001,又可表示為:1.0110010001 乘以 2的111次方(111是7的二進制表示)
- 123.45用十進制科學計數法可以表示為1.2345x10的2次方,其中1.2345為尾數,10為基數,2為指數。浮點數利用指數達到了浮動小數點的效果,從而可以靈活地表達更大范圍的實數。
字符串編碼
ASCII 碼
在計算機中, 不僅數值可以用二進制表示,字符串也能用二進制表示。上世紀美國制定了一套字符編碼,對英語字符與二進制位之間的關系,加上數字和一些特殊符號, 然后用 8 位的二進制,就能表示我們日常需要的所有字符了,這個就是我們常常說的ASCII 碼
ASCII 碼就好比一個字典,將二進制和字符一一對應。其中我們看幾個典型的例子:
- 小寫字母 a? 在 ASCII 里面,十進制97,也就是二進制的 0 110 0001,而大寫字母 A,十進制65,對應的二進制 0 100 0001
- 需要注意的是,里面的數字,比如數字1,二進制對應0000 0001 在ASCII 里面,表示的其實是字符"1",對應的二進制是0 011 0001
- 字符串 15 也不是用0000 1111 這 8 位二進制來表示,而是變成兩個字符 1 和 5 連續放在一起,也就是0 011 0001和 0 011 0101,需要用兩個 8 位二進制來表示 。所以計算機儲存數據時,二進制序列化會比直接存儲文本能節省大量空間
EASCII:擴展的 ASCII
一開始美國編寫ASCII表,英語用128個符號編碼就夠了,但隨著計算機的普及,西歐國家不全是英語國家,有德語,法語等等
比如 字母上方有注音符號,它就無法用 ASCII 碼表示。于是歐洲工程師就決定,利用字節中閑置的最高位編入新的符號。他們把 ASCII 擴充變成了 EASCII,這擴充的包括希臘字母、特殊的拉丁符號等。由于 ASCII 只占了 7 位,所以 EASCII 把第 8 位利用起來,仍然是一個字節來表示,這時表示的字符個數是 256。
Unicode
但 EASCII 并沒有成功,西歐國家以及各個 PC 廠商各自定義出了好多不同的編碼字符集,ISO-8859將西歐國家的編碼一起包含進去。但隨著計算機來到中國,那些歐美國家把 現有的字典都用完,而且漢字有十多萬個,所以急需新的"字典"。GB2312編碼就出來了,使用兩個字節表示一個漢字(漢字太多),所以理論上最多可以表示 256 x 256 = 65536 個符號。后來GBK編碼 將古漢字等生僻字加進來。臺灣地區又創造了BIG5編碼,再后來GB18030對東南亞地區的文字,進行了統一。簡單了解下這些編碼,就不具體展開了
再后來計算機全球普及,各個國家地區的文字編碼太多太亂,Unicode編碼的出現,為了統一全世界的所有字符。Unicode 是一個很大的集合,現在的規模可以容納100多萬個符號。
由于Unicode 只是一個字符集(Charset),它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲,也就是字符編碼(Character Encoding),這就導致計算機無法區別 Unicode 和 ASCII ,比如三個字節表示一個符號,而不是分別表示三個符號呢?
隨著互聯網的崛起,UTF-8 就是在互聯網上使用最廣的一種 Unicode 的實現方式。UTF-8 它是一種變長的編碼方式。它可以使用1~4個字節表示一個符號,根據不同的符號而變化字節長度。Unicode 字符集中的大部分漢字,如果用 UTF-8 編碼的話,是占 3 個字節的。
下面我們看看UTF-8是如何兼容Unicode的:UTF-8編碼致力于統一世界上所有的字符集,所以它的設計上既向下兼容ASCII碼的編碼方式,同時又考慮了可拓展性,規則如下:1)對于單字節的符號:字節的第一位設為0,后面7位為這個符號的 Unicode 碼。與 ASCII 編碼規則相同;2)對于n字節的符號(n > 1):第一個字節的前n位都設為1,第n + 1位設為0,后面字節的前兩位一律設為10。剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼。
Unicode | UTF-8 | byte 數 |
0000~007F | 0XXX XXXX | 1 |
0080~07FF | 110X XXXX 10XX XXXX | 2 |
0800~FFFF | 1110 XXXX 10XX XXXX 10XX XXXX | 3 |
1 0000~1F FFFF | 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX | 4 |
我們可以發現,UTF-8 編碼的第一位如果是 0,則只有一個字節,跟 ASCII 編碼完全一樣,所以兼容了。如果是 110 開頭,則是兩個字節,以此類推如上表所示。所以開頭幾位的值,是編碼本身,同時又是判斷后續還有幾個字節數的推碼(通過推碼才能判斷這個字節之后還有幾個字節共同參與一個字符的表示)
亂碼的來源
編碼是把數據從一種形式轉換為另外一種形式的過程,而解碼則是編碼的逆向過程。編碼是一種格式,解碼是另一種格式,當然會出問題。下面我們舉個例子,來看看這個問題:
- 創建hello.txt文件,用Notepad++打開編輯,以 UTF-8 格式寫入你好
- 然后我們改變Notepad++的formaat格式,改為GB2312,然后你好就變成了浣犲ソ
在UTF-8 字典中,你好?兩個字的16進制編碼分別是E4BDA0、E5A5BD?在GB2312字典中,浣犲ソ?三個字的16進制編碼分別是 E4BD、A0E5、A5BD
由于在UTF-8 編碼漢字是 3 個字節,在GB2312編碼漢字卻是2個字節,計算機用GB2312去解析UTF-8,硬生生的把3個字節以每2個字節為一組去解碼,所以才會有出現這種亂碼。當我們知道亂碼出現的原因,如何解決就變的非常簡單了
參考資料:
《編碼:隱匿在計算機軟硬件背后的語言》
《深入理解計算機系統 第三版》
《計算機組成原理》
《深入計算機組成原理》
https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
https://www.zhihu.com/question/20159860
https://blog.csdn.net/f919976711/article/details/116714860
本文轉載自微信公眾號「 小牛呼嚕嚕」,作者「小牛呼嚕嚕」,可以通過以下二維碼關注。
轉載本文請聯系「小牛呼嚕嚕」公眾號。