位運算的那些事(一)搞懂機器碼

最近在開發過程中查看Android源碼,多處看到一些類似result = specSize | MEASURED_STATE_TOO_SMALL;的寫法,乍一看很熟悉,實際閱讀起來很痛苦,這是我們大學裏學過的位運算,單看代碼似乎我們不可能一瞬間知道結果是多少,所以千萬要和我們常見的result = a || b區分開來。以此爲引子我們就瞭解一下有關位運算的那些事。

位運算主要針對二進制,它包括了:“與(&)”、“或(|)”、“非(~)”、“異或(^)”。從表面上看似乎有點像邏輯運算符,但邏輯運算符是針對兩個關係運算符來進行邏輯運算,而位運算符主要針對兩個二進制數的位進行邏輯運算。

理解位運算,必須先了解二進制在計算機中轉換過程,這也是本篇所講的重點內容。這些明白了,針對一些複雜的簡單的運算法則也就很清晰了。

機器碼和真值

在談二進制之前我們先了解兩個簡單的概念:“機器碼”和“真值”。

機器碼

一個數在計算機中的二進制表示形式,叫做這個數的機器碼。機器碼是帶符號的,在計算機用一個數的最高位存放符號,正數爲0,負數爲1。比如,十進制中的數+1,計算機字長爲8位,轉換成二進制就是00000001,如果是-1 ,就是10000001。那麼,這裏的00000001和10000001就是機器碼。

真值

在機器碼中第一位是符號位,但是我們拋去這個規則,就得到不知正確的數值了,還是上邊的例子-1,用機器碼來表示爲10000001,但如果表示成十進制的真值則等於129,這差別就大了。或者你可以總結真值轉換成二進制和機器碼的區別就是有沒有符號之說,符號你自己定義。

原碼, 反碼, 補碼

上邊有了機器碼和真值的概念,從這裏開始我們就揭開計算機是如何將“-1”這類真值轉換成機器碼參與計算或存儲起來的。首先我們要明白二進制數在內存中是以補碼的形式存放的,這裏又引申出原碼反碼補碼的概念,也是二進制位運算的基礎,後邊會說。

原碼

前邊我們已經說到機器碼是帶符號的,所以我們的真值轉換成機器碼的時候要根據其符號正或者負分別將其轉成首位的0和1。

例:
[+1] = [0000 0001]原
[-1] = [1000 0001]原

注:這裏按照單個字節來表示,java上int類型是四個字節,就不用過於糾結了。

因爲第一位是符號位, 所以8位二進制數的取值範圍就是:[11111111, 01111111],即[127, -127]。

反碼

同樣,反碼也是區分符號的,正數的反碼是他本身, 負數的反碼是符號位不變其餘按位取反

例:
[+1] = [0000 0001]原 = [0000 0001]反
[-1] = [1000 0001]原 = [1111 1110]反

補碼

和上邊一樣,補碼也是區分符號的,正數的補碼是他本身,負數的補碼是在反碼的基礎上加1

例:
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]補
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]補

由上邊三個概念我們得到兩個結論:

  1. 正數的原碼、反碼、補碼都一樣,負數根據原、反、補規則做變換
  2. 計算機將一個真值最終存儲或參與計算是需要經過這幾步:真值->原碼->反碼->補碼

計算機中爲什麼要以補碼形式參與運算或存儲

我們都知道,CPU是由運算器、寄存器、總線構成。單說運算器說的簡單點就是有一個個小開關構成的集成器,每一個開關的狀態代表0和1,每一次運算都是有由時鐘脈衝將這些狀態帶出來或存儲在寄存器中…(不好意思,扯遠了)。對於一個有符號的真值轉換成機器碼做運算,人的大腦很輕鬆的就能判斷處理,但是如果讓計算機集成器中一個個小開關去保存這些狀態,那麼每個集成器上還要預留一個開關做爲正負,並且其餘開關進行轉換的過程中還要兼顧符號開關的狀態,這樣是不是很繁瑣。

聰明的人們在想能不能將真值中的符號位直接參與到運算呢?這樣就可以不用在硬件中專門預留一個這樣的符號開關標識了。我們知道, 根據運算法則減去一個正數等於加上一個負數, 即: 1-1 = 1 + (-1) = 0 , 所以機器可以只有加法而沒有減法, 這樣計算機運算的設計就更簡單了。

例如計算十進制的表達式: 1-1=0,以下循序漸進給出計算機爲什麼最終採用補碼形式計算的原因。

原碼計算

上邊表達式用原碼計算:

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

如果用原碼錶示, 讓符號位也參與計算,顯然對於減法來說,結果是不正確的.這也就是爲何計算機內部不使用原碼錶示一個數的原因。

反碼計算

爲了解決原碼做減法的問題, 聰明的人們又想到了反碼計算:

1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

發現用反碼計算減法, 結果的真值部分是正確的. 而唯一的問題其實就出現在"0"這個特殊的數值上. 雖然人們理解上+0和-0是一樣的, 但是0帶符號是沒有任何意義的. 而且會有[0000 0000]原和[1000 0000]原兩個編碼表示0.

補碼計算

補碼的出現, 徹底解決了0的符號以及兩個編碼的問題:

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]補 + [1111 1111]補 = [0000 0000]補=[0000 0000]原

這樣0用[0000 0000]表示, 而以前出現問題的-0則不存在了,而且可以用[1000 0000]表示-128:

(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]補 + [1000 0001]補 = [1000 0000]補

-1-127的結果應該是-128,在用補碼運算的結果中,[1000 0000]補就是-128。但是注意因爲實際上是使用以前的-0的補碼來表示-128, 所以-128並沒有原碼和反碼錶示。(對-128的補碼錶示[1000 0000]補算出來的原碼是[0000 0000]原,這是不正確的)。

使用補碼, 不僅僅修復了0的符號以及存在兩個編碼的問題,而且還能夠多表示一個最低數(多開闢一個存儲空間)。這就是爲什麼8位二進制使用原碼或反碼錶示的範圍爲[-127, +127],而使用補碼錶示的範圍爲[-128, 127]。

總結

本篇系統了闡述了一個真值轉化到計算機所最終使用的機器碼的一個過程。一個真值最終被計算機所使用會經過真值->原碼->反碼->補碼這些步驟,但如果計算機計算完之後呢,當然要逆過來走一遍,由補碼->反碼->原碼->真值這個過程最終得到一個被人腦所認知的正確結果。

開篇提到了二進制的位運算,但是不摸清原碼、反碼、補碼這些基礎很難去得到位運算的正確結果的。下一篇我會以本篇的基礎來講一下二進制位運算相關運算符的運算規則。

參考

  • cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html
  • https://www.zhihu.com/question/20159860
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章