前言
int a=12;
int b=1.234;
問:a 和 b 在計算機中到底是如何存儲的?
答:轉為二進制?
問:轉成二進制就直接存儲了?
答:...
問:小數 b 如何轉成二進制的?
答:...
我們日常更多的時候都在使用十進制的數,要知道我們的祖先可厲害了,應該很長一段時間都在使用 16 進制。譬如一個成語叫“半斤八兩”,解釋:半斤、八兩輕重相等,比喻彼此不相上下。等等!半斤和八兩怎么相等,對的,宋代一斤就是等于十六兩,當然半斤等于八兩。
都知道計算機使用的是二進制,只用 0 和 1 就能表示數字,但是計算機到底是如何解決數字存儲的問題?下面分別討論整數和小數的存儲:
如何存儲整數
假設我們現在有一個 8 位的操作系統,最高位是符號位,1 表示負數,0 表示正數,所以能表示的最大的整數區間為 11111111 ~ 01111111 => -127 ~ 127。
但是在存儲表達的時候會遇到一個問題,10000000 和 00000000 兩個 0 的問題,+0 和 -0 應該也是相等的,這給電路設計上帶來了很多麻煩和多余的計算規則。
十進制轉成二進制
簡單點的記憶就是:除 2 取余再倒序。
- 12 / 2 = 6..0
- 6 / 2 = 3..0
- 3 / 2 = 1..1
- 1 / 2 = 0..1
0011 倒序 即為 1100,轉成八位即在前面補0,則結果為 0000_1100。
0000_1100 是 java8 中的寫法,我覺得看起來比較舒服且前后連貫,_ 是為了看起來比較清楚人為添加的,后面沿用這種寫法,0000_1100 = 00001100= 0000 1100
都是相等。
一個天才的設計師
看了很多的文檔沒有找到解決這個問題的作者是誰?不管是誰反正提出了補碼
這個概念,真的是有效的解決了這個問題,不僅解決了 0 這個問題還帶來了一個更大的優點,后面補充。
正數的原碼、反碼、補碼
正數原碼 = 反碼 = 補碼
- 12 的原碼是 0000_1100
- 12 的反碼是 0000_1100
- 12 的補碼是 0000_1100
這里并沒有廢話,盡是科學。
負數的原碼、反碼、補碼
反碼 = 原碼符號位不變其他位取反
補碼 = 反碼 + 1
- -12 的原碼是 1000_1100
- -12 的反碼是 1111_0011
- -12 的補碼是 1111_0100
0 的存儲
+0
- 原碼:0000_0000
- 反碼:0000_0000
- 補碼:0000_0000
-0
- 原碼:1000_0000
- 反碼:1111_1111
- 補碼:0000_0000
這里應該把前面的問題都解決了吧,補碼不僅解決了 +0 和 -0 的問題,還神奇的把符號位都去掉了,計算機在運行的過程中從而可以更簡單的進行運算。
減法運算
為了效率,計算機底層計算的時候是沒有減法運算,減法運算都轉成加法運算。
12 - 12 => 12 + (-12) => 0000_1100 + 1111_0100 => 0000_0000
- 0000_110
0
+ 1111_0100
= 0 + 0 = 0 - 0000_11
0
0 + 1111_010
0 = 0 + 0 = 0 - 0000_1
1
00 + 1111_01
00 = 1 + 1 = 0 進 1 - 0000_
1
100 + 1111_0
100 = 1+ 0 + 進1= 0 進 1 - 000
0
_1100 + 1111
_0100 = 0+ 1 + 進1= 0 進 1 - 00
0
0_1100 + 111
1_0100 = 0+ 1 + 進1= 0 進 1 - 0
0
00_1100 + 11
11_0100 = 0+ 1 + 進1= 0 進 1 -
0
000_1100 +1
111_0100 = 0+ 1 + 進1= 0 進 1
因為只能存儲 8 位,最后一個進 1 爆掉就剩下 0000_0000 了。
這里都是以 8 位計算機進行舉例,現在的 32 位或者 64 位計算機都是同理的。
如何存儲浮點數
相信很多人小數轉成二進制都是不知道如何運算的,更別談存儲了,下面我就娓娓道來。
文章剛開始編輯的時候將浮點數稱為小數,由于發現這樣不專業,其實小數不一定都是浮點數,在那個沒有標準各自為政的早計算機時代其實還是有定點數,感興趣可以看參考文檔。
浮點數轉成二進制
1.234 在計算機中轉成二進制是按照是分整數部分和小數部分進行的,整數部分上面以前講到,這里說下小數部分 0.234 為例:
簡單點的記憶就是:乘 2 取整再順序。
- 0.234 * 2 = 0.468 => 整數部分為 0 => 取 0
- 0.468 * 2 = 0.936 => 整數部分為 0 => 取 0
- 0.936 * 2 = 1.872 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.872 * 2 = 1.744 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.744 * 2 = 1.488 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.488 * 2 = 0.976 => 整數部分為 0 => 取 0
- 0.976 * 2 = 1.952 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.952 * 2 = 1.904 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.904 * 2 = 1.808 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.808 * 2 = 1.616 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.616 * 2 = 1.232 => 整數部分為 1 => 取 1 (并將整數部分抹去)
- 0.232 * 2 = 0.464 => 整數部分為 0 => 取 0
- ...
下面就不寫下去了結果是 0.001110111110...,寫的越長精度越高。
可見浮點數存儲必將是一個頭疼的問題,浮點數底層的邏輯肯定也是比整數更為復雜。
IEEE754 標準
電氣電子工程師學會(英語:Institute of Electrical and Electronics Engineers)簡稱為 IEEE,IEEE754是專門規定浮點數該如何存儲的一個標準,規定了四種表示浮點數值的方式:單精確度(32位)、雙精確度(64位)、延伸單精確度(43比特以上,很少使用)與延伸雙精確度(79比特以上,通常以80位實現)。
任意一個浮點數都可以表示為:
(-1)^s 表示符號位,當 s=0,V 為正數;當 s=1,V 為負數。
M 表示有效數字,大于等于 1,小于 2。
2^E 表示指數位。
例子:V = 0.234(十進制) = 0.001110111110(二進制) = 1.110111110 * 2^-3 ,則 s=0 ,M= 1.110111110,E=-3。
IEEE754 規定:
對于 32 位的浮點數,最高的 1 位是符號位 s,接著的 8 位是指數 E,剩下的 23 位為有效數字 M。
對于 64 位的浮點數,最高的 1 位是符號位 s,接著的 11 位是指數 E,剩下的 52 位為有效數字 M。
IEEE754 還有一些特殊的規定:
針對 M
由于 1<= M <=2 ,所以 M 始終為 1.xxxx 形式,xxxx 表示小數,那些對于計算機底層嚴苛的設計師這時候又要將 1 這一位舍掉,只保留 xxxx 部分,這樣的好處是可以多儲存一位有效數字。
針對 E
由于 E 是一個 8 位的無符號存儲,只能表示 0 ~ 255,現實中的指數還存在負數,所以規定:E必須再加上一個中間數,對于 8 位的 E,這個中間數是 127;對于 11 位的 E,這個中間數是 1023,這樣就可以將指數的表達范圍擴大到 -127 ~ +128
和 -1023 ~ +1024
。
實際中的取值范圍是 -126~+127,-127 和 128 被用作特殊值處理,雙精度同理。
阮一峰的博客中寫的是 減去一個中間數 應該是有誤的。
舉個例子,如果 E=17,則 17+127 =144 ,實際的存儲指為 144。
針對 E 還有一些特殊值的情況
- 如果指數 E 是 0 并且尾數的小數部分是 0,這個數是 ±0(和符號位相關)。
- 如果指數 E 是 1 并且尾數的小數部分是0,這個數是±∞(同樣和符號位相關)
- 如果指數 E 是 1 并且尾數的小數部分非0,這個數表示為不是一個數(NaN)。
計算
V = 0.234(十進制) = 0.00111011111001110110110010(二進制) = 1.11011111001110110110010 * 2^-3 ,則 s=0 ,M= 1.110111110,E=-3。
根據 IEEE754 標準轉化 :
以 單精度為例
s 不變
E=-3+127=124(十進制) = 0111_1100(二進制)
M=110_1111_1001_1101_1011_0010
結果將他們連接 0_0111_1100_110_1111_1001_1101_1011_0010(s_E_M)
可以在線校驗結果
小結
這篇文章詳細介紹了計算機如何存儲整數以及浮點數,個人也是從朦朧狀態到理解透徹,參考了眾多大佬的文章,在此表示感謝。有問題的朋友可以通過郵箱聯系到我 jake.zou.me@gmail.com
。
計算機之美妙不可言啊!