天天看點

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,非常自然

當然有優點就一定有缺點,缺點就是二進制的書寫是非常不友善的,可讀性也很差,這也是很多語言為什麼會需要彙編來做一個中間過程~,起碼彙編的可讀性比二進制強很多,另外基于彙編還可以做一些代碼的優化~

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

計算機怎麼計算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個圖,讓你一眼就看明白

c++ 輸出二進制_探索二進制的世界[人類智慧的結晶]
c++ 輸出二進制_探索二進制的世界[人類智慧的結晶]
c++ 輸出二進制_探索二進制的世界[人類智慧的結晶]

如果看不明白的話,請檢視原作者的解釋,在這裡

現在我們都學到了,原來正真在最底層吭哧吭哧進行運算的,都是二進制補碼~

為什麼會溢出?

理論上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,

c++ 輸出二進制_探索二進制的世界[人類智慧的結晶]

這些在計算上的表現就是低位進位導緻高位溢出,是以符号位就是不斷的被“取反”,丢掉的高位,就好比是時鐘走完了一圈,進入下一圈後上一圈就沒了,同樣,補碼的設計,就實作了減法變成加法的運算,比如我們在計算127+1,補碼運算得到的結果是-128,二進制的 1000 0000,那麼實際的值就是結果的補碼,對-128的補碼,就是用“模”-|-128|(注意:這裡的補數計算,一定是絕對值,正數就是正數本身,負數,就是絕對值),就等于256-128 = 128,是以127 + 1 = 128

然後,我們用一段c++代碼來看下這個答案:

#include

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

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的問題~

可點選檢視原文,了解文中圖檔的釋義。如對文中内容有疑問,歡迎評論,與作者一起探讨。

關注印記中文

c++ 輸出二進制_探索二進制的世界[人類智慧的結晶]