探索二進制的世界[人類智慧的結晶]

序言

最近在看一點關於計算機編程基礎的內容,在講到彙編被轉爲更底層的機器碼的過程當中突然對二進制的一些內容感到很疑惑,一直以來對這塊的內容只有在學校的時候有接(聽)觸(說)過,話很少說,帶着幾個問題,站在大佬們的基礎上,作一個簡單的解釋和總結。c++

準備好紙和筆,有沒明白的地方,能夠手算一下,你會發現不少計算機先人們智慧的結晶~面試

計算機爲何要用二進制來運算

使用二進制的計算對實現計算機來講不是一個充分必要條件,理想條件下能夠有N進制的cpu,可是考慮如下問題:編程

  1. 物理實現複雜度:若是用二進制的話門電路的導通與截止,電壓的高壓與低壓,均可以完美的表示二進制的全部數字0和1,若是是10進制,實現0-9這10個穩定狀態的電路和電壓,是比較困難的,不過已經有人在研究3進制計算機了~
  2. 運算實現複雜度:對N進制的求和或者求積各有N(N+1)/2種,對於二進制來講就是2*3/2=3種,好比加法,分別是 0 + 0 = 0; 0 + 1 = 1; 0 + 1 = 10;若是換成10進制的話,就會有10*11/2 = 55種,這對於計算機的實現來講也是至關複雜的。
  3. 電路的0,1能夠想象成沒電和有電,這種條件下電路的穩定性是比較可靠的,若是化成10份,抗干擾能力急劇降低,會出現不合預期的干擾狀況,所以鑑於機器的可靠性,二進制是最優的解。
  4. 最後就是邏輯判斷很是方便,1是true 0是false,很是天然

固然有優勢就必定有缺點,缺點就是二進制的書寫是很是不方便的,可讀性也不好,這也是不少語言爲何會須要彙編來作一箇中間過程~,起碼彙編的可讀性比二進制強不少,另外基於彙編還能夠作一些代碼的優化~優化

綜合而言,二進制天生符合計算機的脾氣~ui

計算機怎麼計算1-1的?

這是個展現人類先進思惟的地方,首先,咱們須要知道一點,二進制的計算過程沒有減法,好比計算1-1 會被轉化成 1+(-1),實現一個減法的過程不是不能夠,而是對計算機的成本太大,代價也很大,尤爲要考慮到減數,被減數,以及結果的正負,轉換成加上一個負數,能夠統一計算過程(都是加法),大大減少了計算的複雜性。spa

咱們以8位的二進制數字來解釋,偉大的計算機先人們爲了解決正負數的問題,把一個二進制的首位定義爲一個數字的正負,因此 1 的二進制原碼是 0000 0001,-1的二進制原碼是 1000 0001;當正真開始計算的時候,問題出現了:設計

十進制 二進制原碼 計算結果
1 0000 0001
-1 1000 0001
操做 加法 1000 0010

二進制原碼:原碼是指將最高位做爲符號位(0表示正,1表示負),其它數字位表明數值自己的絕對值的數字表示方式。3d

what,驚人的發現結果是十進制的-2,這不是想要的結果,同時由於首位數字是符號位的緣由,會致使2個0,0000 0000表明+0,1000 0000 表明-0,基於以上的問題,偉大的先人們發明了反碼,反碼:若是是正數,則表示方法和原碼同樣;若是是負數,符號位不變,其他各位取反,則獲得這個數字的反碼錶示形式,有了反碼,咱們能夠看看能夠解決哪些問題:code

十進制 二進制原碼 二進制反碼 計算結果
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(至關於將原碼數值位取反而後在最低位加1cdn

有了補碼以後,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個寄存器,存放除數,被除數,中間數,以及商,最後的餘數就直接用被除數-中間數就能夠了~

因此,儘可能能用位移,不用加減,能用加減不用乘除,能用乘法,不用除法~

總結

  1. 二進制都是以補碼的方式在底層作運算;
  2. 有限範圍內的計算溢出會致使不可預期的結果,
  3. 8位的-128是先人們智慧的結晶,固然還有16位的-xx,以及32位的-xx等等
  4. 計算機的世界,沒有減乘除,只有加法~

本文有一些內容是總結和引用,有一些是筆者本身的理解,若有錯誤,請海涵並指出~

不針對浮點數的運算,由於運算方式徹底不一樣於整數

二進制很是簡單,可是又很是的絕妙,若是能仔細體會的話

若是以上內容對你有點幫助,但願能夠給個贊

後續可能會再總結一下爲什麼0.1+0.2!=0.3的問題~

相關文章
相關標籤/搜索