序言
最近在看一點關於計算機程式設計基礎的內容,在講到彙編被轉為更底層的機器碼的過程中忽然對二進位制的一些內容感到很疑惑,一直以來對這塊的內容只有在學校的時候有接(聽)觸(說)過,話不多說,帶著幾個問題,站在大佬們的基礎上,做一個簡單的解釋和總結。
準備好紙和筆,有沒明白的地方,可以手算一下,你會發現很多計算機先人們智慧的結晶~
計算機為什麼要用二進位制來運算
使用二進位制的計算對實現計算機來說不是一個充分必要條件,理想條件下可以有N進位制的cpu,但是考慮以下問題:
- 物理實現複雜度:如果用二進位制的話閘電路的導通與截止,電壓的高壓與低壓,都可以完美的表示二進位制的所有數字0和1,如果是10進位制,實現0-9這10個穩定狀態的電路和電壓,是比較困難的,不過已經有人在研究3進位制計算機了~
- 運算實現複雜度:對N進位制的求和或者求積各有N(N+1)/2種,對於二進位制來說就是2*3/2=3種,比如加法,分別是
0 + 0 = 0; 0 + 1 = 1; 0 + 1 = 10;
如果換成10進位制的話,就會有10*11/2 = 55種,這對於計算機的實現來說也是相當複雜的。 - 電路的0,1可以想象成沒電和有電,這種條件下電路的穩定性是比較可靠的,如果化成10份,抗干擾能力急劇下降,會出現不合預期的干擾情況,因此鑑於機器的可靠性,二進位制是最優的解。
- 最後就是邏輯判斷非常方便,1是true 0是false,非常自然
當然有優點就一定有缺點,缺點就是二進位制的書寫是非常不方便的,可讀性也很差,這也是很多語言為什麼會需要彙編來做一箇中間過程~,起碼彙編的可讀性比二進位制強很多,另外基於彙編還可以做一些程式碼的優化~
綜合而言,二進位制天生符合計算機的脾氣~
計算機怎麼計算1-1的?
這是個展示人類先進思維的地方,首先,我們需要知道一點,二進位制的計算過程沒有減法,比如計算1-1 會被轉化成 1+(-1),實現一個減法的過程不是不可以,而是對計算機的成本太大,代價也很大,尤其要考慮到減數,被減數,以及結果的正負,轉換成加上一個負數,可以統一計算過程(都是加法),大大減小了計算的複雜性。
我們以8位的二進位制數字來解釋,偉大的計算機先人們為了解決正負數的問題,把一個二進位制的首位定義為一個數字的正負,所以 1 的二進位制原碼是 0000 0001,-1的二進位制原碼是 1000 0001;當正真開始計算的時候,問題出現了:
十進位制 | 二進位制原碼 | 計算結果 |
---|---|---|
1 | 0000 0001 | |
-1 | 1000 0001 | |
操作 | 加法 | 1000 0010 |
二進位制原碼:原碼是指將最高位作為符號位(0表示正,1表示負),其它數字位代表數值本身的絕對值的數字表示方式。
what,驚人的發現結果是十進位制的-2,這不是想要的結果,同時因為首位數字是符號位的原因,會導致2個0,0000 0000代表+0,1000 0000 代表-0,基於以上的問題,偉大的先人們發明了反碼,反碼:如果是正數,則表示方法和原碼一樣;如果是負數,符號位不變,其餘各位取反,則得到這個數字的反碼錶示形式,有了反碼,我們可以看看可以解決哪些問題:
十進位制 | 二進位制原碼 | 二進位制反碼 | 計算結果 |
---|---|---|---|
1 | 0000 0001 | 0000 0001 | |
-1 | 1000 0001 | 1111 1110 | |
操作 | 加法 | 1111 1111 |
1111 1111 按照反碼的格式,取反(原碼取反再取反還是原碼本身)過來就是10進位制中的-0,因為 1000 0000 的反碼就是 1111 1111,所以通過反碼的形式,先人們完美的解決了 1 + (-1) = 0的問題,但是上面說到還有一個問題,+0 和 -0 這個現象依然存在,就像壞了一鍋湯的老鼠一樣,偉大的先人們的智慧當然不允許這種情況的存在,於是乎,有人創造了 補碼,補碼:如果是正數,則表示方法和原碼一樣;如果是負數,則將數字的反碼加上1(相當於將原碼數值位取反然後在最低位加1
有了補碼之後,1的補碼是:0000 0001, -1的補碼是 1111 1111 當我們去相加的時候:
十進位制 | 二進位制原碼 | 二進位制反碼 | 二進位制反碼 | 計算結果 |
---|---|---|---|---|
1 | 0000 0001 | 0000 0001 | 0000 0001 | |
-1 | 1000 0001 | 1111 1110 | 1111 1111 | |
操作 | 加法 | (1)0000 0000 |
在反碼的基礎上,補了一位之後,我們發現結果正是我們想要的,而且不會有-0的出現了,但是有得必有失,我們丟了-0,但是我們獲取了-128,為啥?-0 的補碼是 1000 0000 所以,先人前輩們把這個補碼定義成了當前補碼範圍內可以表示的最小的負整數,8位的二進位制就是-128,這也是為啥8位二進位制表示的數字範圍是[-128, 127]的原因,-0 丟了,但是加了一個-128,現在我們通過補碼的形式把這個壞老鼠成功的剔除掉了,一切的計算看起來都是那麼的完美~
現在我們完整的走一次計算過程,以1-2=-1來實現:
十進位制 | 二進位制補碼 | 計算結果 |
---|---|---|
1 | 0000 0001 | |
-2 | 1111 1110 | |
操作 | 加法 | 1111 1111 |
結果是負數,所以想知道它具體是多少需要通過補碼來檢視,所以 1111 1111的補碼是 1000 0001 就是十進位制的-1
為什麼對負數結果要求補碼? 其實我們運算的過程就是用的補碼,那麼理論上應該是反補碼才能拿到實際的資料,but but 這裡為什麼正向求補碼了?這就是二進位制非常神奇的地方,一個二進位制的原碼的補碼的補碼就是 -----> 這個原碼本身(就好像一個原碼的反碼的反碼還是原碼一樣自然),正數因為補碼永遠是自己,所以肯定是成立的,對於負數,驗證如下:
1000 0001 求補 1111 1111
1111 1111 求補 1000 0001
所以,對於計算機來說運算之前求一次補碼,運算之後再求一次補碼,就可以拿到正確的結果拉,一切都是那麼的自然。。
不好理解?放2個圖,讓你一眼就看明白
如果看不明白的話,請檢視原作者的解釋,在這裡
現在我們都學到了,原來正真在最底層吭哧吭哧進行運算的,都是二進位制補碼~
為什麼會溢位?
理論上N位二進位制所能表達的數字一定是有限的,比如8位二進位制的範圍就是[-128, 127],當計算到127的時候,+1 就會"跳"到-128,就像一個圓圈一樣,一切都回到了原點重新開始,只是這個臨界點不是“0”而是“127”和“-128”,所以,溢位是一定會出現的,當計算結果超出當前range範圍,就會產生溢位的行為,理解這個行為,我們要先理解“模”這個概念;
假如給一個鐘錶,因為鐘錶的範圍一共就是12個格子,所以“12”就是它的“模”,超過12就會重新計算,這種現象,就是“溢位”,在看下面的例子:假如現在時針在2點的位置,如果我想要他變為6點,有幾種辦法,理論上有N種,我可以不停的旋轉然後再回來,我們討論最基本的,其實是2種,正向走過4個格子,到6,這就是 2 + 4 = 6;還可以反向走 8 個格子,[2>1>12>11>10>9>8>7>6] ,會發現一個絕妙的點:4+8=12,同時,4和8就是對於“模”12的一對補數,在鐘錶上我們可以看出來2-8==2+4 往後退8個就等於往前走4個,也就是說,在“模”運算中,x-y==x+y的補數,回到二進位制,二進位制的計算和鐘錶的計算是非常像的,8位二進位制的“模”就是256,從[0-127]以及[-1,-128],各有128位數字,到達臨界點的時候就會break到下一個原始的點,比如從-128 再走就回到 +127,從+127再走就到了-128,
這些在計算上的表現就是低位進位導致高位溢位,所以符號位就是不斷的被“取反”,丟掉的高位,就好比是時鐘走完了一圈,進入下一圈後上一圈就沒了,同樣,補碼的設計,就實現了減法變成加法的運算,比如我們在計算127+1,補碼運算得到的結果是-128,二進位制的 1000 0000,那麼實際的值就是結果的補碼,對-128的補碼,就是用“模”-|-128|(注意:這裡的補數計算,一定是絕對值,正數就是正數本身,負數,就是絕對值),就等於256-128 = 128,所以127 + 1 = 128
然後,我們用一段c++程式碼來看下這個答案:
#include <stdio.h>
int main(void)
{
char a, b;
char c;
a = 127;
b = 1;
c = a + b;
printf("c=%d", c);
return 0;
}
複製程式碼
輸出是-128,這是因為,8位我們知道最大能表示的正數是127,128當然無法表示了,因此會從127 跳到下一位,發生一次符號的反轉,就是-128,這個溢位導致的結果會引起無法預期的bug,如果我們把c的長度換位16位的二進位制:
#include <stdio.h>
int main(void)
{
char a, b;
short c;
a = 127;
b = 1;
c = a + b;
printf("c=%d", c);
return 0;
}
複製程式碼
輸出就是 128~
所以在有位數限制的語言中,一定要注意計算溢位的問題~
二進位制的乘除
二進位制乘法
從上面的描述我們知道了二進位制只有加法,沒有減法,那麼有的小夥伴肯定會有疑問了,減法都沒有,那乘法怎麼辦?我們看看計算機通過二進位制是怎麼解決乘法問題的:
從我們已知的十進位制說起,假設我有一個數字900.000,我現在想對900做乘法,算900*10=? 從我們接觸小數點的時候,老師應該都說過,遇到*10的情況,我們就數一下乘數有幾個0,1後面幾個0就把小數點往右邊移動幾位,所以這裡我們把小數點往後移動一位,結果就是9000.00,現在我們忽略小數點,和後面的0,發現乘以10的過程,實際就是把900全部左移了一位,最後補了一個0,對吧,也就是每次的左移1位代表對被乘數乘以10,右移一位就表示除以10~
回到二進位制的世界,理論都是一樣的,只是逢十變成了逢二而已,但是巧就巧在,10進位制中我們的乘數可能不是10的整數次冪,比如*5,*3,但是二進位制情況下,所有的數字都是2的整數次冪,比如我有一個二進位制的資料 0000 0001 乘以 10 (注意,10是十進位制的2),那就把被乘數所有的數字全部往左移動一位,就代表乘以2沒問題,所以結果就是0000 0010 高位捨去,低位補0~,所有的二進位制的複雜乘法都是通過這個方式(移位+加法)來實現的,我們看個比較複雜的例子:
1111 * 111
首先,乘數 111 分別表示是2º、2¹、2²,什麼意思,就好比十進位制的9 * 110 可以分解為 9 * 10¹ + 9 * 10²,對應就是9向左移1位得到90 + 9向左移2位得到900 = 990,同理,對於二進位制來說 0就是不移位,那就是1111,1表示左移1位,就是11110 ,2就是左移2位,就是111100,然後計算二進位制加法
加法計算:
0000 1111 | |
0001 1110 | |
0011 1100 | |
加法 | 0110 1001 |
得到結果 0110 1001 轉化為10進位制就是:
15 * 5 = 105
完美,所以可以看到整個計算過程都是通過移位+加法來解決問題的,所以,現在你應該知道為什麼面試中問你計算2³最快的方式是2<<2了嗎?
二進位制除法
除法的運算相對複雜和耗時一些,還是以10進位制的計算過程為例:
計算 19 / 3 = ?
因為我沒有乘除,只能用加法來解決問題,但是我知道除數和被除數,那麼,我先用一個除數和被除數比,發現 3 < 19,那麼給我的除數再加上一個除數,一直繼續,整體過程就是
3 < 19 商 + 1 當前中間計算結果 3
6 < 19 商 + 1 當前中間計算結果 6
9 < 19 商 + 1 當前中間計算結果 9
12 < 19 商 + 1 當前中間計算結果 12
15 < 19 商 + 1 當前中間計算結果 15
18 < 19 商 + 1 當前中間計算結果 18
21 > 19 停止 此時商從0變為 6 餘數為 19 - 18(中間數)= 1
計算得出餘數為1 商為6 返回計算結果~
其實就是不停的累加除數的過程,一直到找到第一次累加之和超過被除數的上一次為止~,剩下的就是餘數
當然,換做二進位制,流程是一樣的,只是都用二進位制去計算~,我就不細說了,除法還是比較麻煩的,需要用到至少4個暫存器,存放除數,被除數,中間數,以及商,最後的餘數就直接用被除數-中間數就可以了~
所以,儘量能用位移,不用加減,能用加減不用乘除,能用乘法,不用除法~
總結
- 二進位制都是以補碼的方式在底層做運算;
- 有限範圍內的計算溢位會導致不可預期的結果,
- 8位的-128是先人們智慧的結晶,當然還有16位的-xx,以及32位的-xx等等
- 計算機的世界,沒有減乘除,只有加法~
本文有一些內容是總結和引用,有一些是筆者自己的理解,如有錯誤,請海涵並指出~
不針對浮點數的運算,因為運算方式完全不同於整數
二進位制非常簡單,但是又非常的絕妙,如果能仔細體會的話
如果以上內容對你有點幫助,希望可以給個贊
後續可能會再總結一下為何0.1+0.2!=0.3的問題~