數學是一個完全抽象的學科,而電腦是這個學科的一種形象化的實現,顯然無法處理一些僅在抽象意義上有意義的特殊“數字”,比如無窮之類的東西,。像數學中的加法,乘法這樣運算,電腦必須給與實現,然而由於數學中的實數加法(以及別的運算)是建立在實數域上的,而實數域又是無限的,而電腦只能處理有限域的運算,因此必須給定一個範圍,一種方案是在這個範圍內保證運算的正確性,超出範圍的結果給出錯誤提示,然而這樣的電腦不是很完美,畢竟它能力太有限了,僅僅給出錯誤提示是不能讓人滿意的,如果能在有限的編碼上實現諸如實數運算那樣“無錯誤”運算就更好了,畢竟給出錯誤提示並不是一個很合理的計算結果,我們不需要一個提示,而是在任何情況下(包括超出範圍)都可以得到一個“能夠自圓其說的正確”結果,因此電腦採用了在有限域上的加法運算後再次模數的方式將結果控制在一個有限的範圍內。
電腦是基於模運算來進行加法和乘法運算的,在實現編碼之前,必須有一個理論支撐,對於加法,必須實現一個阿貝爾群,它要滿足交換率,擁有一個“零”,每一個元素都存在一個逆,相加後等於零,並且結果也應該在該群中,只要按照如此的規則設計一個編碼,那麼對於電腦而言就非常圓滿了,對於任何數位加法,我們都不會得到一個“錯誤”,而是很實在的得到一個正確的結果,如果實數可以表示在數軸上,那麼電腦處理的數就可以表示在一個圓環上。為了方便,以下將電腦表示數的範圍僅考慮成位元組,實際上現在電腦表示8位元組的數已經不是什麼稀罕事了。
一個位元組可以表示從00000000到11111111範圍內的256個數字,由於電腦編碼不區分有符號和無符號數,按照和實數的一致性,我們將其視為有符號數, 這樣的話負數和正數應該各佔一半,它們中間有一個0,由於十進位數和位元僅僅是數制的不同,不管在什麼數制下,0都是不變的,那麼顯然需要將0編碼為00000000。現在考慮一下這樣做的後果,8位位元的最小值被編碼成了0,那麼負數怎麼辦呢?畢竟沒有比0更小的數字了啊!想象一下,我們現在做的事是“編碼”,一個數位編碼只是一種表示方式,它真正的含義是其字面上的值,而不是編碼的值,比如說,我們可以將數字1編碼成“0”,而將數字0編碼為“123”,僅此而已。為了表示負數,我們先從-1開始考慮,在字面上,-1加上1就是0,而1的二進位編碼就是00000001(二進位和十進位僅僅數制不同,編碼正數和0當然就是完全按照字面意義進行數制轉換編碼的,編碼負數其實就是編碼一個符號“-”),其最低位就是1,根據二進位加法,要想x+00000001的結果為00000000,x的值必然是11111111,但是不對啊,8位的1和00000001加在一起是100000000而不是00000000,也就是說,結果是9位,超出了一個位元組。不過這沒有關係,我們的前提是電腦只能表示8位的數字,那麼最高位的1當然算是溢出了,我們得到的結果就是00000000,因此-1的編碼就是11111111,接下來-2呢?很顯然-2+1=-1,因此-2為11111110,依次類推,我們得到的最小的負數為10000000,這是為什麼呢?設想還能表示比它小1的負數x,則x+1=100000000,這樣的話x的值就是01111111了,我們姑且將這個數看成是比10000000還小1的一個負數,接下來考慮一下正數,我們知道1的編碼是00000001,2的編碼是00000010,依次類推,我們也得到了01111111這個數,這是因為電腦將它能表示的數字編碼在一個圓環上而不是一個數軸上,那麼01111111到底是正數還是負數呢?這時候考慮一下阿貝爾群的性質,如果將01111111編碼為負數,那麼它將和它的逆元相加結果為00000000,既然電腦算術僅僅最後做了模數操作,其它的群性質和實數域的是一樣的,因此01111111的逆肯定是正數,我們求出它的逆是10000000,而這個數是我們從-1開始的負數編碼推匯出來的一個負數編碼,因此這樣的假設不正確,所以01111111必然屬於一個正數的編碼,它的十進位數是127,它是電腦能表示的最大的整數,而10000000則是電腦能表示的最小的負數,它的十進位值是-128。
如果最大的正數01111111加1的結果是什麼呢?結果是10000000,變成了最小的負數,這也是合理的,因為電腦將數字編碼在了一個圓環上。既然電腦將數字編碼在一個圓環上,那麼如果我們將此圓環從10000000處劈開並拉直,將得到一個線段,上面編碼的數字從10000000開始是遞增的,直到01111111。有了這個形象化的概念之後,下面看一下加法的實質,一個數x在圓環上,另一個數y肯定也在圓環上,x+y的計算過程則是維持x不變,將00000000到y的這一段圓弧ar摘下來,拼接到x處,最終的ar的終點處的編碼就是結果,事實上ar有兩種摘取方式-順時針和逆時針,不管哪種方式,最後結果落到同一處。電腦加法的結果左後按照圓環的周長模數,並不是說先得到實數域的字面結果,然後再執行模數操作,而是按照前面設計的圓環,二進位加法最後的模數是自動進行的,本質上說這個“自動”是通過高位的溢出來實現的。注意,我們現在討論的是編碼,而不涉及有無符號,具體有符號還是無符號,就看指令的解釋了,10000000可以是有符號的-128,也可以是無符號的128,同樣,11111111可以是有符號的-1,也可以是無符號的255。
編碼和符號是無關聯的,那麼模數是如何自動實現的呢?設想250+10的操作,字面操作的結果是260,可是電腦最大隻能由8位來表示,因此按照無符號解釋最大也只能表示為11111111,也就是255,可是260和255還相差5,顯然結果需要在255的基礎上再加上5,根據剛才的形象化的圓環加法,加上5之後落到了00000100這個位置,也就是十進位數4,這個4就是就是最後的結果,一個圓環的周長是256,4正好是260除以256的餘數,而取餘數就是模數。在電腦硬體上,這是通過溢出來實現的,255的二進位表示為11111111,它加上1之後就發生最高位溢出到第9位,由於電腦只能表示8位的數字,因此丟棄最高位,結果為[1]00000000([]中的被丟棄),一切從0開始,再加上4,結果就是4。不管在這個圓環上繞幾圈,最終的落點都在圓環上,每繞一圈就會發生最高位的溢出,然後從00000000開始,這就是模數的實質。
對於乘法,這個圓環依然使用,兩個數相乘,只要有一個數是正數,那麼就可以用上述的圓弧不斷拼接來得到答案,比如3*2就是用2個3這個圓弧拼接兩次,-2*4就是拿4個-2這個圓弧拼接四次,對於兩個負數相乘,比如-a*-b,總是可以拆成a*b*-1*-1,只需要計算-1*-1即可,也就是11111111*11111111,列出遞等式進行計算,最終的結果是[xxx]00000001,高位全部溢出了,結果就是正數1,在實數乘法中,我們依靠了一個規定“負負得正”,然而在電腦編碼中,負號本身和數字一起被編碼,依靠溢出竟然可以推匯出“負負得正”這個所謂的規定,顯然,電腦是不能被“規定”的,只有在純抽象的領域才能規定,在電腦上,必須實現這個規定,將負號和數字一起編碼可以解決這個問題,直接了當的推匯出了一切在實數域上的異號數字乘法的性質。
加法和乘法都套入這個“圓環+溢出”模型之後,最後再來看一下0這個特殊的數字,在實數域上,-x+x=0也是一個規定,一個群的性質,然而在電腦,它卻是被真實地實現的,也是依靠溢出,不失一般性,以-1+1為例,1的編碼為00000001,要想得到-1的編碼和1加和等於00000000,同時又沒有對電腦的行為進行“規定”,根據二進位加法,只要能得到[x[y]]00000000就可以了,8位以上的不用管,由於電腦只能表示8位,8位以上的全部溢出即可,比如11111111+00000001=[1]00000000,就這樣實現了群性質中的逆元加和等於零元的這個性質。
教科書上經常說原碼,反碼,補碼之類的概念,還說電腦一律使用補碼編碼,正數和0的補碼等於原碼,負數的補碼等於負數相反數的原碼的反碼加1。其實根本不需要這麼複雜的概念,僅靠以上的“圓環+溢出”模型就能說明一切,什麼原碼,補碼之類的概念根本就沒有必要引出來,之所以這麼引出來是為了符合科學精神,任何事物都要有概念,概念的匯出是一個分析和綜合的過程,在形成概念之前必然需要歸納出一個一般結論,接下來才可以形成概念,由此可以想象,當初的編碼設計者也是先想到了上述的“圓環+溢出”這個形象化的模型,進而才引出“原碼,反碼,補碼”這些抽象的概念的,有了形象模型,再解釋為何負數的補碼是其相反數的反碼加1就簡單多了,首先要明白負數和其相反數相加等於00000000,而由於電腦沒有被“規定”而是真實實現了群的性質,因此這個00000000實際上應該是100000000,溢出了一個1到第9位,我們知道一個正數和該正數取反之後相加,會得到11111111這個數字,然而再加上1則會得到[1]00000000這個所謂的0,正數的二進位編碼完全按照數制的轉換機制由十進位(或者其它進位)數轉化而來,因此它不能變,而負數由於需要編碼一個“負號”,因此它需要被改成別的,因此就由“去掉負號的該數按位取反然後加上1”來表示負數,其中包含了兩個部分,第一部分是負號,第二部門為該負數的相反數。最終,表示負數的時候,其相反數就成了“原碼”,得到負數編碼的中間步驟-按位取反操作的結果顯然就是“反碼”,而結果加1則稱作補碼,為了一致性,正數的補碼就是原碼!按照有符號資料解釋,所有的負數的最高位都是1,而所有的正數最高位都是0,因此最高位也就成了負號位,注意,負號“-”的編碼已經隱藏在了“取相反數-按位取反-結果加1”的整個過程中了,因此最高位雖然是符號位,它卻不是負號“-”的編碼,你不能說負號的編碼是10000000。
如果教科書上不是先從原碼,反碼,補碼這類概念講起,而是先引出一個“圓環+溢出”的模型,最後通過這個模型自然而然匯出這些概念的話,我想很多學生就不會被這些概念搞得糊塗且無助了。文藝複興以來,西方科學精神風靡,要知道這種精神的本質和古代純思辨推理精神的本質只差那麼一點點,那就是“概念”要通過“現實的實現”來檢驗,任何概念都是通過歸納法得到感性認識,然後再通過推理得到一般性的表述,對於電腦編碼以及任何其它的工程學原理而言,熟悉這個歸納的過程是最重要的,要比你熟練得掌握概念要有益得多,只有當你理解了這個歸納的過程,再去深入這些概念才會覺得輕鬆,理解概念的目的是將知識體系化,而只要理解歸納的過程才能真正學會這個概念。
附:關於編碼的符號
c語言中提供了有符號和無符號兩種類型的資料類型,其實這僅僅指示如何解釋這個資料類型,對於彙編層次的編碼來說是不區分符號的,比如11111111可以解釋成無符號的255,也可以解釋成有符號的-1。那麼c語言的關於有無符號的資料類型是給誰看的呢?其實是給指令看的,指令負責解釋一個資料是有符號的還是無符號的,和大部分人的想象不一樣,這種指令非常少,按照常理,理應每個算術指令都應該提供兩套才對,一套針對有符號資料,另一套針對無符號資料,實則沒有這個必要。這是因為電腦的數字編碼不區分符號,甚至沒有大小,它完全是一個封閉的阿貝爾群,兩個數相加前怎麼解釋,它們的和就怎麼解釋,因此完全沒有必要設計兩套指令,加法的過程完全符合本文中的圓弧拼接法則。本文中假設電腦最多用8位編碼資料,然而如今的電腦可以用8位,16位,32位等不同的位元來編碼資料,那麼就涉及到“將一個x位的資料放到一個y位的空間中”這種事情,在電腦中,實際上每一種資料都表示一個圓環,如果系統中有8位,16位,32位三種資料的話,系統中就有三個圓環,只有將一個資料從一個圓環放到另一個圓環的指令才會考慮符號,比如現有8位編碼為11111111,現將其放入16位的圓環中,如果它是無符號數,顯然它是255,因此16位編碼為0000000011111111,然而如果它是有符號數,那麼它就是-1,它的16位編碼就是1111111111111111,可見放到16位圓環中的位置是不同的,因此只有在這種情況下才需要考慮符號,故電腦硬體在這種情況下提供了指令組,比如movzx/movsx等等,在同一圓環上進行的運算根本沒有必要考慮符號,因此也就不需要提供兩套指令。