原碼、反碼、補碼的產生、應用以及優缺點有哪些?
我在網路上搜到的都是結論,即"原碼是...反碼是..."
一、序言
第一版答案寫於2016年8月,當時我正試圖理解補碼規則的邏輯,並用結果寫了一篇回答發在知乎和公眾號上,因為收到的回復很樂觀,讓我一度認為已經把握問題的全貌。事實上答案在符號位的論述上存在謬誤,多虧知友在回復中指出,為此我進行了更深入的思考,重新編輯此答案,希望能更接近問題的本原。
二、重溫運算規則
首先我想把整套關於原碼反碼補碼的運算規則準確清晰地寫一遍,方便急需應用的知友參考,也希望大家首先能記住這套規定,再開始進一步的探討。
所謂原碼就是機器數,是加了一位符號位的二進位數,正數符號位為0,負數符號位為1,計算機中存儲、處理、運算的數據通常是8位、16位、32位或64位的,這裡以最簡單的8位為例講解。注意符號位是包含在8位中的其中1位,故可直觀讀出的數只有7位(只有後7位數可以按權展開)。有心人可能注意到原碼是有缺陷的,它只能表示255種狀態,因為00000000(+0)和10000000(-0)其實是一個數,因此原碼的表示範圍成了-127到+127,這個問題需要神奇的補碼來解決,因為在補碼中10000000被用來表示-128。
所謂反碼,英語里又叫ones" complement(對1求補),這裡的1,本質上是一個有限位計數系統里所能表示出的最大值,在8位二進位里就是11111111,在1位十進位里就是9,在3位十六進位里就是FFF(再大就要進位了)。求反又被稱為對一求補,用最大數減去一個數就能得到它的反,很容易看出在二進位里11111111減去任何數結果都是把這個數按位取反,0變1,1變零,所以才稱之為反碼。用原碼求反碼的方法是,正數不變,負數保留符號位1不變,剩下位按位取反。
所謂補碼,英語里又叫two"s complement(對2求補),這個2指的是計數系統的容量(模),就是計數系統所能表示的狀態數。對1位二進位數來說只有0和1兩種狀態,所以模是10也就是十進位的2,對7位二進位數來說就是10000000,這個模是不可能取到的,因為位數多一位。用模減去一個數(無符號部分)就能得到這個數的補,比如10000000-1010010=0101110,事實上因為10000000=1111111+1,稍加改變就成了(1111111-1010010)+1,所以又可以表述為先求反再加1。總結求補碼的方法就是正數依舊不變,負數保留符號位不變,先求反碼再加上1。
記住了怎麼求補碼,接下來講講運算。通過原碼的符號位和數值,我們能迅速指出它代表的數,判斷其正負並進行四則運算,相比而言反碼和補碼對於人則顯得過於晦澀。如果說原碼是給人看的數字語言,那麼補碼就是計算機的數字語言。計算機不需要知道什麼是正負、大小,這些判斷對它而言過於複雜。事實上它存儲、處理、傳輸的數都只有補碼一種形式,人所做的加減乘除,在計算機里只通過相加和移位就能解決,這都來自於補碼系統的內在自洽和巧奪天工的神奇魔力,也是後文要闡述的重點。
對加法和減法,按上文的方法求得補碼之後,直接相加就可以了,但相加的時候符號位一定要一起參與運算,有時候,兩符號位相加或者接受來自低位的進位會發生溢出,就扔掉溢出的一位(稍後會解釋為什麼),由新的符號位決定結果的正負,如果是0表示正數,結果就是原碼,如果是1表示負數,結果還要再求補數得到原碼。
至此我介紹了原碼反碼補碼的規定,以及如何求補碼並進行加減法(乘除暫不涉及,事實上懂了加減法的奧秘,乘除很容易理解),對於一個工程人才來說,上面的內容已經足夠應付所有具體問題。剩下的則是一些「無用」的思考,關於為何這套法則能夠化減為加,以及人為規定的符號位在運算中為何總是能精確地指示結果的符號。
三、無用之用
數字是用來記錄現實世界數量屬性的語言。
而任何計數系統都必須有兩個參數:容量和精度。
模是衡量計數系統容量的參數。模代表了計數系統所能表示和存儲的狀態數。
任何有限的計數系統都有一個確定的模。如時鐘的模是12(即只有一個位的十二進位系統,若再加一個大鐘,使小鍾轉一周大鐘加一刻度,就是有兩個位的十二進位系統),再比如8位計算機的模是2^8=256D(每一位也可以單獨看做一個模為2的計數系統)。
問題一:化減為加
對同一計數系統中的數量可以定義運算如加減,但運算結果超出預設位數時,就要發生溢出,這個溢出其實就是模,是時鐘的一整圈(因此丟掉它沒有影響),如果進位沒有被另一個計數系統接受,結果看似「失真」,本質上是進入了「第二次循環」。
以時鐘系統為例:8+7=15D=13(十二進位)&>10(十二進位),進位1溢出丟失(除非用另一個時鐘接收這個進位),在錶盤上(即一位十二進位計數系統中)呈現為3,而8-5=8+(-5)=3也得到了相同結果。這就說明在有限容量的計數系統中,+7和-5是完全相同的,而它們正是關於模12的一對補數。因此我們在有限的計數系統做了這種定義:正數補數即為本身,負數A【補】=模-絕對值(A)。一個數加上另一個數(可以是正數也可以是負數),結果等於加上這個數的補數,若有進位則捨棄進位。這麼做的重大意義在於極大地方便了計算機進行數據處理,要知道對人而言減法並非難事,但用門電路實現就複雜得多了,減之前還要判斷大小考慮次序。
問題二:符號位參與運算
在8位計算機中,一個位元組可以表示256種狀態,把位元組看做一個鐘的話,刻度可以隨便標,不如取0點鐘為-128,正對的6點鐘為0,即存儲範圍是從-128到127,用二進位補碼錶示是10000000~01111111(10000000用來表示-128似乎是人為定義的,因為原碼無法表示-128,按正常程序更無法求得其補碼)。
我舉個例子,你們感受一下:
所謂的「負數加負數會變成正數,正數加正數會變成負數」,本質還是在於,計數系統是無法表示超出其取值範圍的計算結果的。
120D+120D=01111000B+01111000B=11110000B,符號位的1來自低位進位,指示了結果是負數,所以需要求補得10010000B也就是-16D,放在鐘面上就是從120順時針旋轉120格到240的位置,只不過系統最大隻取到127,240的位置就是-16的位置,而且-16和240正是關於模256的一對補數。-120D-120D=16D也是一樣的道理。在有限的計數系統內,由於位數的限制,發生溢出的情況下無法得到計算真實值,得到的是真實值關於模的補數。
看到這裡是不是有那麼點味道呢,我給你們總結一下:加法都是從低位往高位做的,如果兩個數(補碼),後七位相加產生了進位,說明又溢出了一次,每當溢出一次(就是越過了-128這個正負分界點),符號就要反一下,0變1,1變0。符號是1的,說明大得越界了,需要再求個補,用取值範圍內的負數表示結果;符號是0的,說明小得越界了,但由於正數的補數就是本身,就不必再求補了。四、後記
從八月底的初稿到這篇文章,中間經歷了差不多四個月的時間,我對於補碼問題的認識也經歷了困惑到清晰到困惑到再清晰這一過程,其中修改超過十次,思考所花的時間更是不計其數。從參加考試的角度看,我熟記的運算規則早已足夠我應付所有題目,但我仍然不願意半途而廢,原因有二:
大一學習線性代數時,曾經掛過科,因為對於定理和公式背後的含義一無所知,而老師也不加講解,只一味讓我們死記做題。雖然很多同學都適應這種所謂的「工科數學學習」,然而這對我而言簡直如同夢魘,沒有理解內化如何能稱得上學習,不過是應付考試然後忘的精光罷了。我很幸運的是,在準備補考時讀到了網上廣為流傳的孟岩老師的文章《理解矩陣》,我記得那是一個冬天的晚上,讀完文章後我很興奮,一直到半夜都睡不著,這是我第一次體會到數學體系的和諧自洽以及數學的深刻性在工程中的巨大威力,從那以後我才逐漸找到了學習數學的樂趣。
《理解矩陣》中有一段話至今我還記得,現摘抄如下:
自從1930年代法國布爾巴基學派興起以來,數學的公理化、系統性描述已經獲得巨大的成功,這使得我們接受的數學教育在嚴謹性上大大提高。然而數學公理化的一個備受爭議的副作用,就是一般數學教育中直覺性的喪失。數學家們似乎認為直覺性與抽象性是矛盾的,因此毫不猶豫地犧牲掉前者。然而包括我本人在內的很多人都對此表示懷疑,我們不認為直覺性與抽象性一定相互矛盾,特別是在數學教育中和數學教材中,幫助學生建立直覺,有助於它們理解那些抽象的概念,進而理解數學的本質。反之,如果一味注重形式上的嚴格性,學生就好像被迫進行鑽火圈表演的小白鼠一樣,變成枯燥的規則的奴隸。「枯燥的規則的奴隸」又何止是在數學教學中出現的呢?如果你在大學工科學習過,你會發現這些人簡直遍地都是,拿我在的浙大為例,有的是學生對課程並不理解,單靠考前突擊刷題就拿到90分以上的成績。
正是在這樣的情形下,我決定盡我所能重新思考學到的每一個重要知識,並將其中一部分寫成文章,一來有助於對思維的梳理,二來也是便於自己將來的回顧,倘若拙作還能對他人也有所幫助,從而使我給世界留下一些微不足道的影響,那真是幸甚了。原碼、反碼、補碼的產生、應用以及優缺點有哪些?
我嘗試硬生生的把它們串起來哈
數字在自然界中抽象出來的時候,一棵樹,兩隻豬,是沒有正數和負數的概念的
計算機保存最原始的數字,也是沒有正和負的數字,叫沒符號數字
如果我們在內存分配4位(bit)去存放無符號數字,是下面這樣子的
正如上帝一揮手,從混沌中劃分了「白天」與「黑夜」
為了表示正與負,人們發明了"原碼",把生活應該有的正負概念,原原本本的表示出來
把左邊第一位騰出位置,存放符號,正用0來表示,負用1來表示
但使用「原碼」儲存的方式,方便了看的人類,卻苦了計算機
我們希望 (+1)和(-1)相加是0,但計算機只能算出0001+1001=1010 (-2)這不是我們想要的結果 (╯" - ")╯︵ ┻━┻
另外一個問題,這裡有一個(+0)和(-0)
為了解決「正負相加等於0」的問題,在「原碼」的基礎上,人們發明了「反碼」
「反碼」表示方式是用來處理負數的,符號位置不變,其餘位置相反
過去的(+1)和(-1)相加,變成了0001+1101=1111,剛好反碼錶示方式中,1111象徵-0
人們總是進益求精,歷史遺留下來的問題—— 有兩個零存在,+0 和 -0
我們希望只有一個0,所以發明了"補碼",同樣是針對"負數"做處理的
"補碼"的意思是,從原來"反碼"的基礎上,補充一個新的代碼,(+1)
我們的目標是,沒有蛀牙(-0)
有得必有失,在補一位1的時候,要丟掉最高位我們要處理"反碼"中的"-0",當1111再補上一個1之後,變成了10000,丟掉最高位就是0000,剛好和左邊正數的0,完美融合掉了
這樣就解決了+0和-0同時存在的問題
另外"正負數相加等於0"的問題,同樣得到滿足
舉例,3和(-3)相加,0011 + 1101 =10000,丟掉最高位,就是0000(0)
同樣有失必有得,我們失去了(-0) , 收穫了(-8)
以上就是"補碼"的存在方式
結論:保存正負數,不斷改進方案後,選擇了最好的"補碼"方案以前寫過一篇blog: 補碼、負數和減法,儘管不是很對題,但依然希望能給題主帶來幫助---背景
複習c++的時候遇到二進位編碼問題,上網搜索了一番,終於有點眉目。 一般來說,初學二進位編碼時,會看到如下描述:
原碼錶示法是機器數的一種簡單的表示法。其符號位用0表示正號,用:表示負號,數值一般用二進位形式表示。
機器數的反碼可由原碼得到。如果機器數是正數,則該機器數的反碼與原碼一樣;如果機器數是負數,則該機器數的反碼是對它的原碼(符號位除外)各位取反而得到的。
機器數的補碼可由原碼得到。如果機器數是正數,則該機器數的補碼與原碼一樣;如果機器數是負數,則該機器數的補碼是對它的原碼(除符號位外)各位取反,並在未位加1而得到的。
如果是為了考試,死記即可。但我總想搞清楚為什麼計算機裡面的數要這樣子表達?意義何在?-128的補碼為什麼是10000000?為什麼補碼有這麼奇怪的運算規則?計算機算減法的時候都需要從源碼到補碼的計算嗎?
思路google了一下,看到了這樣一篇文章,注意到文中關於補碼來歷的描述,可以總結如下:
- 計算機裡面,只有加法器,沒有減法器,所有的減法運算,都必須用加法進行。
- 用補數代替原數,可把減法轉變為加法。出現的進位就是模,此時的進位,就應該忽略不計。
- 二進位下,有多少位數參加運算,模就是在 1 的後面加上多少個 0。
- 補碼就是按照這個要求來定義的:正數不變,負數即用模減去絕對值。
作者的意思是說,計算機裡面所有數都以補碼形式保存,加減運算都是補碼之間的加法運算。然後作者提出了一個我之前沒聽過的觀點:
補數 和 補碼的定義式 裡面,根本就沒有什麼符號位。這最高位的1、0是自然出現的,並不是由人來規定的。
的確,符號位在補碼運算裡面是「模」,本身並不帶符號的意義。因為計算機將加法轉換成加上一個「負數」,而負數又以補碼的形式表現。補碼比源碼多一位,從這多出來的一位可以推斷出原來數字的正負號,所以成為了符號位。也可以這樣認為,留出一位(不全部佔滿)的原因是要用「模」來表示正負數。
也就是說,不是特意留出一個符號位,用1和0來表示正負號。而是補碼運算可以用最高位來表示正負,所以符號位誕生了。
那麼為什麼-128的補碼是10000000?可以這樣理解。-128是一個負數,所以它的補碼是它的「模」減去它的絕對值,即:
100000000 - 10000000 = 10000000
那麼為什麼負數補碼等於源碼的反碼加一呢?可以這樣推導:
100000000 - 10000000
= (11111111 + 00000001) - 10000000
= 11111111 - 10000000 + 1
= 01111111 + 1 //反碼加一
= 10000000
由此我們得知,在計算機裡面所有的數字都以補碼形式存儲。127存成01111111,-127存成11111111,算減法就變成算加法了,儘管你看到的是「-」號。
-------
今天讀《計算機組成-結構化方法》後,對這個問題有了新的理解。
將負數用補碼錶示,實際上是實現了一種從[-128, 127]到[0, 255]的映射。如下所示:
+----------------------------+
| 255 -1 11111111 |
| 254 -2 11111110 |
| 253 -3 11111101 |
| 252 -4 11111100 |
| 251 -5 11111011 |
| 246 -10 11110110 |
| 236 -20 11101100 |
| 226 -30 11100010 |
| 216 -40 11011000 |
| 206 -50 11001110 |
| 196 -60 11000100 |
| 186 -70 10111010 |
| 156 -100 10011100 |
| 129 -127 10000001 |
| 128 -128 10000000 |
| 127 127 01111111 |
| 100 100 01100100 |
| 70 70 01000110 |
| 60 60 00111100 |
| 50 50 00110010 |
| 40 40 00101000 |
| 30 30 00011110 |
| 20 20 00010100 |
| 10 10 00001010 |
| 5 5 00000101 |
| 4 4 00000100 |
| 3 3 00000011 |
| 2 2 00000010 |
| 1 1 00000001 |
| 0 0 00000000 |
+----------------------------+
不請自來。我來說說補碼,上計算機或者數字電路之類的課的時候,關於補碼老師一般只是說幾個結論,講講運算,對於原理卻語焉不詳。答主是個愛刨根問底(笑)的逗逼,於是上網查過不少牛人的看法,不過最後發現網上的結論大多跟課本上差不多,含糊了事,大概人家也不在乎這些細枝末節(霧)。。好了下面說說我是怎麼刨這個根的~雖然大概不會有人轉載,但是還是希望假如假如有的話可以著名出處~————————正文———————說到補碼,先要提出一個概念:模這個模是什麼呢?它是一個計數概念,相當於三角函數里的周期,或者鐘錶的錶盤。直觀一點說,我們有一個能顯示兩位數字的顯示器,就像定時炸彈那種(霧)。。它顯示的數字從00一直遞增到99,之後呢?之後就是100啦,可是這100它有三個顯示不了啊那可怎麼辦呢?對了,它又從00重新開始了。像不像一條銜尾蛇~那麼這個100就是我們說的模啦。這和我們的補碼有什麼關係呢?有,補碼就是按照這個原理設計的!我們發明補碼是為了解決計算機無法進行減法運算的問題(事實上以前是有減法器的,但是硬體開銷太大了所以被淘汰掉了)。怎樣解決的呢?舉個例子,假設有兩個在模M內的非負數a,b ,計算a-b的結果。假如我們不會算減法怎麼辦呢?我們可以這樣。類比周期函數,我們知道,對於模為M的計數容器(加上M對結果沒有影響,反正M在容器里無法顯示,相當於鐘錶轉了一圈等於沒動)a-b=a-b+M=a+(M-b)所以我們通過計算a和(M-b)的和可以求出a-b的結果。M-b稱為-b的補數,在數字電路中求一個數的補數是不需要減法的哦!這樣,我們不但解決了減法的問題,也一併解決了負數的表示與運算呢。誒?等等,怎麼就解決啦?答主的腦筋怎麼像兔子一樣跳來跳去的!嘿嘿。我們回顧一下剛才那個式子。a-b=a+(M-b)發現什麼問題沒有?是的,右邊的式子一定是個非負數,而左邊不一定哦。假如a<b,a-b=c那麼右邊是什麼呢M+c沒錯!是c的補數!所以在計算機中,我們可以把負數用補數來表示,運算起來也很方便。可惜還有一個問題,怎麼區分正數和負數的補數呢?這可難倒了計算機科學家的頭髮(大霧)。。有了!我們有個天才的主意!我們取一個偶數模,一半用來表示正數一半用來表示補數不就行了嗎!來試一試。還是假如有個定時炸彈(BOOM!)它只能顯示兩位數字,那麼我們可以這樣00到49都是正數本身,而50到99卻表示100-50到100-1也就是-50到-1。這樣可行嗎?可以進行我們所有的加減運算嗎?讓我們試試。10-20=10+100-20=90正是-10的補數。然而30+40=70可是70是個負數的補數啊!這可怎麼辦呢?沒關係!因為我們的計算機使用的是二進位。對半分這個概念太適合二進位了。於是我們的補碼實際上是這樣設計的:以八位的一個寄存器為例,00000000~01111111是正數,10000000~11111111是負數的補數。發現什麼特點沒有?是的!正數全是0開頭的,而負數的補數全是1開頭的,它們已經自己分好陣營了!所以我們甚至可以這樣做,讓計算機裡面直接存儲負數而不是補數,等到計算的時候再轉換就好。具體怎麼辦呢?你看,正數全是0開頭的是吧,那我們完全可以用1開頭來表示負數,也就是說,後七位表示數值(和正數一樣),第一位就變成符號位了。這樣的表示在數學裡是錯誤的,不可以直接運算,但是它夠直觀。至於運算,轉換成補碼就可以了嘛。怎麼轉換呢。比如說有一個負數,-00001010(它在計算機中存儲成10001010),怎樣變成補碼呢?觀察結構我們發現,把它取反得到11110101,與本身數值相加得到11111111這是什麼?這是模M減去1啊!也就是說要取補碼我們把原來的負數數值取反再加1就得到了。首位被我們佔用為符號位了,那一位本來的0已經變成1了就不用取反了。只有一個特例就是10000000,只有它的符號位本來就是1,不是我們強行變的。但是我們發現把它第一位固定為1取補後也還是它本身(還是使用在取補前後符號位不動的規則),並且符合所有運演算法則,於是也按照之前的方法運算就好(給設計電路省了不少麻煩)。回到到上邊那個十進位的例子40+30=70=-30的缺陷,在我們設計了電路之後,這個缺陷就不存在了,電路中所有相加結果都會進行檢校,兩個首位為0的數字相加得出1開頭的數會被判斷溢出。而對於不超過表示範圍的數字,我們的補碼第一位符號位可不是規定出來的哦,我們只規定了負數的第一位是符號位,補數可不是哦,它就是模M中後一半的數,和正數一樣,第一位也是有效的數字,運算結果是完全正確的。(哎呀答主說不清楚了。。),總結一下,正數和補數是用來算的,負數的表示方法是用來看的,不能直接參与運算,必須轉化成補數。所以我們8位的寄存器可以表示-128到127一共256個數字。補碼就是這樣被設計出來的~答主表達能力不好,望海涵,鞠躬。
要不是我今天心情好搜了一下,發現了反碼和原碼,我都忘記了他們的存在……然後我去翻logical design的課件,發現原碼和反碼各只佔了一行課件,補碼有好多頁。後面設計電路啊什麼的都是拿補碼的……這幾種碼呢,都是為了表示負數而產生的,所以他們的正數表示方法是一樣的。原碼呢,就是後面不動,第一位表示符號。5是0101,-5是1101.優點是,人能直接念出來1101是-5。缺點呢,你把0101和1101加起來,是啥?反正和0沒啥關係吧…對,缺點就是沒法直接加。反碼的負數是把原碼「符號位不變,數值位取反」。如-5原碼是1101,所以它的反碼就是1010;老師只給了一行課件,我跟它真的不熟,我不知道他有什麼用,個人揣測它是原碼到補碼的一種過渡形態。也就是說,我們可以快速從反碼知道原碼和補碼。補碼,用補碼錶示負數就是反碼+1。-5的反碼是1010,所以它的補碼就是1011。它的優點是,可以把負數直接拿來算加法。後面再來講講補碼……
騷包:我們今天講一下4bit的二進位數的負數是怎麼表示的(只講補碼)。
眾:好。可是為什麼是4bit的啊。
騷包:因為4bit的會了,8bit,16bit的也瞬間會了。舉4bit為例子的話我就可以少寫幾個0.
眾: ……
騷包:我們要表示負數,希望我們的負數能有以下特性:
1:能和正數之間不混淆。
2:保留負數-x=0-x這樣一個特徵(x是一個正數),這樣負數能直接參与到已定義的加法之中。
對於1:我們只需要規定第一位是0的數是正數或0,第一位是1的數是負數就可以啦。這樣4bit的二進位數的話,有7個正數(1~7),一個0以及8個負數。
對於2:我們只需要讓我們的負數 負ABCD=0000-ABCD,其中ABCD代表一個正數,就可以了。
眾:我們不會小數減大數啊!!!
騷包:對於一個4bit的數,我們可以理解為任何一個很大的數的後4位,那麼EFGH和1EFGH和10100101010101EFGH是一樣的啊。我們只要把0000變成一個大的數字就可以啦,為了計算方便,取10000就可以。
因為被減數的倒數第四位是0,所以減去一個正數以後的差的第一位是1,符合第一條,負數第一位是1。通過這種方法我們很快就可以定義出-1~-7。
這樣4bit的16個數中只剩1000沒有被定義啦。1000首先應該是一個負數,所以是10000減去一個正數得到1000。經過計算我們可得10000-1000=1000。
眾:可是1000是負數啊?
騷包:因為我們的4bit只有7個正數,而有8個負數。所以有一個負數所對應的正數是4bit無法表示的。
這裡的減數1000實際上是01000,也就是8. 10000-01000=1000,即1000代表的是-8.
我們到這裡已經把4bit的16個數都表示完全啦~~快樂吧!
眾:每次都這麼算好麻煩啊。
騷包:我教大家一個簡單一個的演算法吧(4bit數中除了-8)。
10000-ABCD=1111+1-ABCD=1111-ABCD+1.
就是說,把一個數取bitwise的inverse,然後+1就好了。
眾:為什麼1111-ABCD相當於取bitwise的inverse?
騷包:1111減一個4bit的數,顯然不借位吧。另外1-0=1,1-1=0,不就相當於取inverse了嘛?
此外,除-8外,通過負數找到對應的正數也是用此法. 令ABCD是我們要找的那個正數,給我們的負數就是10000-ABCD,我們把這個負數做如下操作:
1111-(10000-ABCD)+1=10000-(10000-ABCD)=ABCD。
一個知友問的,回答到這裡方便更多人看到。
有不對請指正,有細節沒寫全請腦補~。「碼」和「數」是兩個東西。
我們平時說出或寫出某「數」,一般都是在十進位下,用10個不同的「碼」(此處的「碼」還和原碼補碼反碼的概念不同)來表示。分別是0~9。超過9,也就是比最大的碼還大的數,採用進位的方式來表示。於是有了「位」的概念。即個位,十位,百位等等。
表達負數的時候由於為了與算術和代數符號當中減號或作差相互兼容,就在該數前面加上減號。這種對一個數的寫法也可以被名門為一種編碼方式。在這種編碼方式下每個數都有一定的「位數」,即「長度」。在計算機當中規定一定的位數稱作「字長」,比如4位,8位,16位,32位等等。只不過,計算機的物理存儲數據是二進位方式。我們人類目前傳統的還是比較喜歡用10進位方式。並且,我們人類一般不會在書寫數字的時候遇到「字長」的概念。但是在念數字和記錄數據的時候,有一點點東西跟字長相關。比如西方愛把數據分成三位一組三位一組的方式,覺得比較好記憶。比如12"000讀成十二千,我們中國人愛以「萬」為字長,1"2000,讀作一萬二千。舉這個例子表示字長的概念不是很恰當。因為對人而言這只是一種表達習慣。但是對機器而言,「字長」則是計算的長度單位。設計計算機進行最簡單的加法運算,首先要考慮的就是加法器進行一次加法運算,位數是多長?加法器設計成多少位的加法器?等等。這就是計算機的字長。它和編碼方式一起,構成了計算機進行計算的基礎。
有了字長的概念才好說補碼。
在十進位下也有補碼的概念。插一句,談到補碼一定要先明確字長的長度。
補碼是用來表示負數的一種編碼方式。也是為了在計算機的核心加法器部分的設計避免減法操作,存儲數據的時候避免存儲負號。舉個例子,假如我們模擬一個字長為4位的十進位計算機。(假設有某種機制,可以在1位上有10種穩定的狀態)對兩個數進行求和運算:1234 和 -1234如果用原碼,那麼我們需要用一個1位的寄存器來存儲和表示負號。假設就在4位的最左邊的最高位前用1表示負號,0表示正號。
則,原碼錶示上面兩個數:0 12341 1234然後計算機做求和,做加法的過程中。計算機發現,其中有一個數是負數,於是要切換為兩個正數相減的模式來運算。這很不方便。於是,補碼被想出來了。
-1234和正1234相加不是等於0000嗎?在4位的字長當中的數,和1234相加為0000的,是不是唯一的一個數哪?很明顯8766和1234相加等於1"0000,後4位是0000。那麼,8766就可以看著是-1234的一種編碼表示方式,被稱作「補碼」。
所以,補碼就是把數1234在一個字長內,補足為1"0000的數的編碼方式。也就是一個正數的相反數,在計算機內的表達方式。
加上符號位,正數0"1234的相反數的補碼錶示就是1"8766。十進位里,-1234的反碼就是1"8765
所以,現在回到4位字長的2進位計算機里來看。補碼和反碼的構成方式是一樣的。0"0001的相反數是-1。
補碼錶示1"1111反碼錶示1"1110看出補碼與反碼的區別了嗎?
反碼就是正數的原碼的相反數的一種構成方式(正數本身的反碼就是它本身,這裡說的是負數的情況),構成方法是按照原碼碼元逐位取反。在十進位里,取反是個難以延伸的概念,其實是以最大的碼元9來減。補碼就是正數的原碼的相反數的另一種編碼方式。它能把字長內的正數,補足為全是0。全爪機手打,有點累~我是搬運工
原碼, 反碼, 補碼 詳解最近學習基礎課的時候又遇到了這個概念,隔了半年再來看 @Lawrence.li的答案,豁然開朗(半年前第一次看的時候我是沒看太懂的), 大神出書了記得@我買。
補充一個我學到的算(負數)二進位補碼的技巧。從大神的最後這張表可以看出,最後補碼和正數的碼形成了這樣一種關係:所有位加起來等於(1)0000,注意這裡其實是5位,但最高位被捨去了。我們可以反過來利用這個進行逆運算。如果小明給你一個正數的碼,比如說1,對應二進位是0001(假設是用4位來表示),小明說要你給他-1的二進位補碼,怎麼算呢?那麼我用(1)0000 - 0001 = 1111,這個就是對應的補碼了。還有一般的計算補碼的方式就是,先用無符號位的二進位表示正數 -&> 最高位取1,變成原碼(負數)-&> 除最高位其他位取反(反碼)-&> 加1(補碼)。假如我要算8位有符號的十進位的-3的補碼,那麼步驟就是:00000011 -&> 10000011 -&> 11111100 -&> 11111101(-3的補碼),跟(1)00000000 (9位) - 00000011 的結果是一樣的。當然,上面說的都是對應有符號的數據類型,如C語言裡面的char和int,如果是無符號的,那就根本用不到補碼了。還有一個值得注意的就是,有符號位的話,符號位一定是最高位(不然沒辦法分辨正負了)。貼一下我的博文(原文:整數為什麼存成補碼? - 賽艇隊長 - 博客園),可能是目前為止最。。。額,自己看吧
背景問題:你知道計算機中以什麼形式存儲整數嗎?是符號位加值位嗎?值位是按照正常的二進位方式存儲的嗎?假如用3位二進位進行存儲,符號位0正1負,1是存成001,-1是存成101嗎?
答:使用補碼的方式而不是正常的方式存儲,雖然是符號位加值位,但符號位承載的信息和值位的值不是你想像中的方式,比如用3位二進位進行存儲,符號位0正1負,1會存成001,-1會存成111
問:計算機為什麼對整數使用補碼的形式存儲
- 出於簡化計算機基本電路的考慮,讓加減法都只需要用加法電路實現。所以需要把減去一個正數或加上一個負數都用加上一個正數的方式來表示,於是在存儲的時候,負數被直接存儲成一種可以直接當成正數加的形式(正數不變,所以以後的討論中有時候略去正數),這種形式就是補碼
問:那麼補碼具體是什麼?補碼是怎麼做到加一個數跟減另一個數一樣效果的?
- 先從時鐘這個身邊的例子理解 「加一個數跟減另一個數一樣效果」
- 假設你對鐘的時候如果發現它是6點,但實際上現在是2點,也就是它走快了4個小時,你可以有兩種做法進行校正,一種是逆時針撥回4個小時到2點,另一種是順時針撥6個小時到12點然後再撥2小時,也就是順時針撥8個小時。也就是對於時鐘的錶盤來說,-4 = +8,同樣還會有 -1 = +11,-5 = +7,甚至還有 -4 = +8 = +20 = +32 = -16
- 他們間隱藏了什麼規律呢?在數學中,-4、+8、+20、+32、-16可以歸為符合某個條件的同一類數字——對於模12同餘。wiki上對於模的定義是 「兩個整數a、b,若它們除以正整數m所得的餘數相等,則稱a、b對於模m同餘」
- 而在一個可溢出計數系統中,把計數系統容量作為模,那麼所有對此模同餘的數在此計數系統中都會有同樣的表示(加這個數也一樣)。比如時鐘錶盤就是一個可溢出計數系統,模為12;一個n位二進位構成的計數系統中,因為會捨棄溢出的高位,所以也是一個可溢出的計數系統,模為2^n(從0數到2^n-1)
- 所以假設一個3位二進位構成的模為8的計數系統,-2,-10,6,14都表示同樣的數,也就是減10和加14是一樣的效果
- 引出「補碼」
- 為了讓「補碼」實現 「加一個正數跟加一個負數(減一個正數) 一樣的效果」,「補碼」就可以是跟原負數對於模同餘的正數
- 在計算機中為了減少不必要的運算,負數的「補碼」就取其中最小的正數,正數直接就是它自己(而且如果負得太多,補一個模都不是正數,那其實算是左邊越界溢出)
- 可能是通過原碼求「補碼」就是一個補模運算,所以把它叫做「補碼」
- (但要注意,這裡的「補碼」都被我打上了雙引號,因為這還不是計算機里真正的存儲的補碼形式,它應該叫補數,不過相信我,已經差不多了)
但這種「補碼」表示還有問題
- 通過轉換成「補碼」,減一個數確實變成加一個數了,看似很不錯,但卻有一個明顯的問題,那就是數本身的符號丟失了
- 我們只存儲了一個在加法運算中方便運算的負數(甚至在結果為負數時計算結果都會不正確),但它卻不是一個能正常表示自己的負數(無法逆運算回去)
- 比如3位二進位,正常能表示0~7,使用補碼法能進一步表示 -8~-1的運算,但不能真正表示 -8~-1
問:怎麼完美解決「補碼」的正負表示問題?
- 不知道人們是怎麼想到的,但最後我們看到的這種做法,是真的Magic。只需要在左邊加一個二進位位來表示正負,就能同時實現這幾個效果
- 在保持補碼特性的前提下。也就是減一個數還是照樣變成加一個數
- 增加正負的表示。能真正表示 -8~-1了,就只用看符號位是0還是1
- 還能讓運算時不用另外區分符號位,直接把符號位當成值位進行運算,而結果的正負號自然會符合這個正負表示法(也就是符號位的進位和值位的進位都會自然地合理)(有一種理解方式是,把這個負號1當成減一個模)
- 具體來說是在左邊加一個符號位,這個符號位不參與「補碼」的運算,始終表示數的正負,但在加法運算中跟值位一樣參與運算。加了一個這樣的符號位的「補碼」就是真正計算機中存儲的補碼了
最後總結正常人怎麼求補碼
- 對負數求最小正同餘數(模為二進位值位存儲容量),把它們放入值位,符號位置1
到這裡「負整數為什麼存成補碼?」這個問題基本就解答清楚了,你會發現裡邊都沒有反碼的影子,對,就是這樣,用反碼以及那套教材里的計算補碼的方法來理解負整數的表示都是緣木求魚,原理上根本不需要它們,那它們是用來幹什麼的呢?值位取反加一這種演算法是怎麼冒出來的?接著往下看補充內容你就知道了
補充計算機中求補碼的簡便演算法
- 對於計算機來說,上面這個過程有點繁瑣,尤其是需要先求模,然後求最小正同餘數,而且這個過程是非常基礎的運算過程,一點點效率提升都能有很大的性能提升
- 所以這個過程被大牛優化成這樣:先直接把負數的絕對值存到值位,符號位為負,然後對值位取反+1,就得到了補碼(這也是很多教材里告訴我們補碼怎麼求的方法。。。想用這個理解補碼,真是。。。)
- 為什麼能這麼優化呢?能看到其實優化的是求「補碼」:最小正同餘數 = 值位取反加一,看上去是不是很神奇,下面證明一下
- 用3位二進位值位[abc]表示一個不會造成溢出的負數F:F = -( a*2^2 + b*2^1 + c)(a,b,c ∈ {0,1})
- 對F的值位取反 :F(反) = (1-a)*2^2 + (1-b)*2^1 + 1-c = 2^2 + 2^1 +1 - ( a*2^2 + b*2^1 + c ) = 2^3 -1 + F
- 然後3位二進位的模等於2^3,所以F的補碼 F(補) = F + 2^3
- 所以結果就出來了: F(反) = 2^3 -1 + F = F(補) -1 得到 F(補) = F(反)+1
拓展內容:對於完美的補碼正負表示方法,雖然不知道大牛怎麼想到的,但這種方式能不能有效果倒是可以嘗試證明一下
大概需要滿足這些條件就表示有效
- 本身的值表示正確
- 本身的符號表示正確
- 運算結果的值表示正確
- 運算結果的符號表示正確
接下來證明每一個條件
1、本身的值表示正確;運算結果的值表示正確
證明:
肯定正確,左邊加一位對值來說是完全不影響的,可以從兩個角度考慮
- 左邊加一個1正好是加了一個模
- 左邊是高位,本身的運算只會向高位進位,完全不會影響低位的運算
2、本身的符號表示正確
證明:不用證明,只要用這個符號系統表示,就會自然正確
3、運算結果的符號表示正確
證明:
可以分幾種情況討論
(假設一個舉例情況:4位二進位,1位符號位,模是8,表示範圍是-8~7)
(把數學數字相加的結果稱為「數學結果」,把通過這種位演算法運算的結果稱為「位運算結果」,這兩個結果的符號一致就是通過)
- 值的位運算不溢出
- 兩個正數
- 數學結果:兩個正數相加還是正數,符號一定為正
- 位運算結果:因為值的位運算不溢出,兩個0相加還是0,也就是正
- 所以兩個結果的符號一致
- 兩個負數
- 數學結果:符號一定為負
- 位運算結果:值的位運算不溢出,兩個1相加進一位得到0,也就是正
- 看起來兩個結果的符號一定不一致
- 但其實可以證明這種「值的位運算不溢出」情況下「數學運算結果一定溢出」,而數學運算結果溢出的情況不用考慮,我們不保證這種情況下運算結果的正確性
- 證明:
- 假設負數A的補碼錶示是【1 a_1 a_2 a_3】,它的數學大小 =-8+(a_1*2^2+a_2*2+a_3),設右側為{A},那麼數學大小 =-8+{A}
- 假設負數B的補碼錶示是【1 b_1 b_2 b_3】,它的數學大小 =-8+(b_1*2^2+b_2*2+b_3),設右側為{B},那麼數學大小 =-8+{B}
- 那麼「值的位運算不溢出」這個條件用數學表示就是 {A}+{B}&<8
- 這時候計算數學運算結果:-8+{A}-8+{B} = -16+{A}+{B},因為{A}+{B}&<8,所以這個結果一定小於-8,而它的表示範圍是-8~7,所以溢出了
- 一正一負
- 正數絕對值大於負數
- 數學結果:正
- 位運算結果:0和1相加得到1,也就是負
- 雖然結果不一致,但是可以證明這種情況不存在,也就是「正數絕對值大於負數」時「值的位運算一定溢出」
- 證明:
- 假設正數A的補碼錶示是【0 a_1 a_2 a_3】,它的數學大小 =a_1*2^2+a_2*2+a_3 ={A}
- 假設負數B的補碼錶示是【1 b_1 b_2 b_3】,它的數學大小 =-8+(b_1*2^2+b_2*2+b_3) =-8+{B}
- 「正數絕對值大於負數」數學表示是 {A}-8+{B}&>0 -&> {A}+{B}&>8
- 「值的位運算」結果為{A}+{B},它一定大於8就說明溢出了
- 正數絕對值小於負數
- 數學結果:負
- 位運算結果:0和1相加得到1,也就是負
- 兩個結果的符號一致
- 正數絕對值等於負數
- 位運算結果需要是0,而如果值位不溢出,符號一定為負,其實通過「正數絕對值大於負數」這個證明可能看出,正數絕對值等於負數時值位也一定溢出
- 另外,這種情況下數學運算結果一定不溢出
- 值的位運算溢出
- 兩個正數
- 兩個正數的值位運算溢出意味著運算結果大於8,意味著數學運算結果溢出,不考慮
- 兩個負數
- 數學結果:一定是負的
- 位運算結果:兩個1相加是0,但是有進位,如果進位為1就沒問題,那進位會不會是2呢?不會,因為後三位最大是7,兩個7是14,不超過16
- 兩個結果的符號一致
- 一正一負
- 正數絕對值大於等於負數
- 數學結果:正
- 位運算結果:0和1相加得到1,溢出1位得到0,也就是正
- 符號一致
- 正數絕對值小於負數
- 數學結果:負
- 位運算結果同上,為正,但這種情況不存在,也就是「正數絕對值小於負數」一定不「值的位運算溢出」
- 證明
- 假設正數A的補碼錶示是【0 a_1 a_2 a_3】,它的數學大小 =a_1*2^2+a_2*2+a_3 ={A}
- 假設負數B的補碼錶示是【1 b_1 b_2 b_3】,它的數學大小 =-8+(b_1*2^2+b_2*2+b_3) =-8+{B}
- 「正數絕對值小於負數」數學表示是 {A}-8+{B}&<0 -&> {A}+{B}&<8,說明值的位運算一定不溢出
證明完畢
剛學計算機導論,同題主一樣,對補碼不怎麼理解,特別是反碼加一這個方法是從哪裡來的,為什麼要用補碼運算,這些個問題。看了前面一些答案大概明白了,下面提供另一種求反碼原理的十進位解釋:設a為二進位表示n-1位的負數,b為其補碼,2^n為模,則有b=2^n-a2^n=1+2^0+2^1+...........+2^(n-1)將a用十進位表示為a=k0*2^0+k1*2^1+..............+kn-1*2^(n-2)由上可知,2^n-a=(1-k0)2^0+(1-k1)2^1+..........+(1-kn-1)2^(n-2)+1+2^(n-1)上式中1-k即為求反碼,後面加1來自2^n的二項式展開中的1,再加上一個2^(n-1)作為符號位。講的不是很明白,見諒。
上面一些答案最多能幫你建立一些直覺和感官上認識,就像劉姥姥進大觀園一樣,只能看個大概的形狀,卻總感覺無法觸摸本質。
想要更深入的理解,推薦看《計算機組織與體系結構》第九章。
從數學的角度上解釋了補碼的作用,更加深刻!用加減乘除的詳實例子解釋了補碼的優美 。真的是」補碼本天成,妙手偶得之「。
最後再輔以看看知乎這些答案,就能get some points。
路給你們指好了,快去吧
上幾張圖
兩個二進位數相加,如果結果溢出。去掉溢出位,與求餘數的作用相當。
補碼是為了把a-b轉化為a+x的形式,其中x是關於b的函數,可以為任何正數。為什麼這個x要用反碼加一的形式呢,因為在電路系統中,取反是相對容易的。下面給出簡單的數學證明,不對請輕噴: 設a,b均為正數,且其同模均為2^n(膜的實際概念為,假設一個數僅有8位,那麼它加減256,所得的結果不變,從數學角度來說,它是一個周期為2^n的函數)。於是,我們得到一個結論:a=a+2^n,b=b+2^n,a-b=a-b+2^n以上結論非常重要。現在,開始證明。a-b=a-[ 2^n -(2^n-b)] =a+(2^n-b)-2^n根據之前的同模結論,a+(2^n-b)-2^n=a+(2^n-b)這樣,終於得到了一個加法式。也就說,我們只要找到一個演算法,使得結果等於2^n-b就行了,哪怕它是積分微分都行。但顯然積分微分太複雜了,電路系統肯定是越簡單越好。於是,我們試著用反碼來表示它。不用證明,設bT為b的反碼,我們有bT+b+1=2^n(這是定義式,可以說是從天上掉下來的,無需證明)於是,bT+1=2^n-b代回原式,得到a-b=a+bT+1,即-b=bT+1證畢================這是一個簡單的證明,不難。以上的結論從工程的角度出發,解釋了數學如何和實際的設計生產掛上聯繫,也解釋了有時候數學的完美和實際工程需求的出入,數學可以天馬行空地n元積分微分,毫無節制地增加拉普拉斯運算的節點,只要得出結論即可。但工程不行,工程希望的是用最少環節獲取到正確的結果,而不是儘可能地發現更多的方法。
哈哈
4位2進位為例
不論怎麼無限加1減1
這4位數字表現為 前三位8種可能 最高位0和1
以一個有8個刻度的鐘來類比 兩圈一個周期
定義其中一圈為負(黑天) 下一圈為正(白天)
這樣8個刻度就有16種數字去對應(12個刻度有24個小時對應)
現在可以這樣來看待一個4位2進位的數 即從0(正圈第一個刻度)開始的代表正轉步數的一個加數
比如1111 最高位為1可以看做從0轉一圈來到負圈-8 前3位111代表再正轉7步 於是來到了-1
即1111=-1
而1111+1111則代表 先正轉兩圈(等於來到下一個周期) 然後正轉7步再7步 來到了負圈的第6步第7個刻度 -2
而-5怎麼對應一個加數呢 -5表示從0反轉5步 即正轉一圈來到負圈再轉3步來到-5
即1011
5的反碼是1010 和1011差了1 所以有個公式是一個數的相反數就是對他取反再加1
2.1.2 數的機器碼錶示個人覺得講解已經很到位了!
我出一道題,看你們理解了沒有。在python中,用~表示按位取反,那麼~(-6)的結果是多少? 寫出計算過程。
碼農不學數學嗎?
建議了解一下同餘的概念,有興趣再看看「中國剩餘定理」;這方面的數學,中國在幾千年前就已經很厲害了。
推薦閱讀:
※舊電腦如何處理才能資源最大限度利用?
※晶圓為什麼是圓的?
※大容量(上T的)固態硬碟存在的意義是什麼?
※一個純粹的程序員,30歲,40歲,50歲,甚至60歲該如何定位?
TAG:計算機 |