上一篇博客我們主要介紹了布爾代數和C語言當中的幾個運算符。那么這一篇博客我們主要介紹在計算機中整數是如何表示的,諸如我們在編碼過程中遇到的對數據類型進行強制轉換可能會得到意想不到的結果在這篇博客里你會得到解答。
1、什么是整數?
整數包含正整數,0,負整數。我們從小的數學常識,整數是無窮無盡的,即整數的大小沒有限制。
但是在計算機中則不能這樣理解,因為計算機是靠數字信號來表示數,計算機所能處理的整數的長度是由計算機的字長來決定的,所以,在計算機中,我們必須制定一個規則來表示整數。
2、C 語言中的整型數據類型
C 語言是支持多種整型數據類型的,下面我們看一下在 32 位機器和 64 位機器中,C 語言整型數據類型的取值范圍。
我們可以看到 :
①、C 語言數據類型是可以用來指定大小,同時還可以指示表示的數是非負數(聲明為 unsigned),或者負數(默認)。
②、數據類型分配的字節數會根據機器的字長和編譯器有所不同,不同的大小所表示的范圍是不同的。上圖唯一一個與機器有關的取值范圍是 long 類型的,64位機器使用8個字節(264),而32位機器使用4個字節(232)。
③、負數的范圍要比正數的范圍大1。這是為什么呢,請接着往下面看。
下面我們看一下 C 語言標准所定義的每種數據類型所能表示的最小的取值范圍。
C 語言標准我們可以從上圖得到:
①、正數和負數的取值范圍是對稱的。
②、int 數據類型可以用 2 個字節來實現。(216)
③、long 數據類型用4 個字節來實現。(232)
3、無符號數的編碼
無符號數,在C語言中,即用 unsigned 聲明的整數。
定義:假設對於一個w位的無符號整數,用二進制比特位可以表示為[xw-1 , xw-2 , … , x2 , x1 , x0]。那么我們可以用一個函數表示如下:
這個函數可以舉幾個簡單的例子來看:
那么很顯然,對於一個無符號編碼的數,由 w 位的二進制序列構成,那么它的最小值,即所有位都為 0 ,用位向量表示即:【000......000】。
UMinw = 0
最大值即所有位都為 1,用位向量表示即:【111......111】
UMaxw = 1 * (1-2w) / 1 - 2 = 2w - 1
我們可以得出一個結論:無符號的二進制,對於任意一個w位的二進制序列,都存在唯一一個整數介於0 到 2w-1之間,與這個二進制序列對應。反過來,在0 到 2w-1之間的每一個整數,存在唯一的二進制序列與其對應。
4、補碼編碼
上面我們講解了正整數的編碼,那么在實際應用中,是存在負數的。而在計算機中,最常見的表示有符號的數就是補碼。補碼的定義如下:
其中最高有效位 xw-1 也稱為符號位,符號位為 1 時表示負數,當設置為 0 時,表示非負數。下面我們看幾個例子:
那么我們可以得出:當最高位為1,其余為全部是 0 的時候,即【1000......000】,表示補碼格式的最小值:
TMinw = -2w-1
當最高位為 0,其余為全部是 1 時,即【0111......111】,表示補碼格式的最大值:
TMaxw = 1 * (1 - 2w-1) / 1 - 2 = 2w-1-1
通過上面的兩個公式,我們就很好理解為什么上面C語言數據類型負數的范圍要比正數的范圍大1。
和上面無符號編碼一樣,我們對於補碼格式編碼也可以得到一個結論:
對於任意一個w位的二進制序列,都存在唯一一個介於-2w-1 到 2w-1-1的整數,與這個二進制序列對應。反過來,對於任意介於-2w-1 到 2w-1-1的整數,存在唯一的長度為w二進制序列與其對應。
那么你就應該明白了為什么十進制 -1,在計算機中二進制表示為 1111 1111,而不是1000 0001,因為計算機是以補碼的形式表示的。
5、反碼和原碼
反碼定義:除了最高有效位的權是-2w-1-1,而不是-2w-1其余的和補碼表示方式一樣
原碼定義:最高有效位是符號位,用來確定剩下的位是正還是負
我們可以和補碼的定義進行對比:
原碼:一個整數,按照絕對值大小轉換為二進制數,最高位為符號位。
反碼:將原碼除最高位(符號位)外,其余各位按位取反,所得到的二進制碼。正數的反碼為原碼。
補碼:反碼最低位加1即為補碼。
對於正整數,原碼、反碼、補碼完全一樣,即符號位固定為0,數值位相同。
對於負整數,原碼和補碼互相轉換的簡便方法:從數的右邊往左開始數,遇到“0”不理它,直到遇到第一個“1”為止,以后的每一位數取反即是它的原碼或補碼,符號位不變,還是“1”(補碼的補碼是原碼)。
比如:11010100 ----- 從右往左數,第一位是0,不理它,第二位還是0不理它,第三位是1,那么從此以后的每位取反,即為它的補碼了.答案為:10101100
事實上,程序員如果希望代碼具有最大的可移植性,能夠在所有可能的機器上運行,就應該用補碼的形式來表示有符號整數。雖然過去生產過基於反碼表示的機器,但是幾乎所有的現代機器都是使用補碼。
注意:浮點數有使用原碼編碼。
關於整型數據類型的表示和取值范圍,Java標准是非常明確的,它要求采用補碼形式,取值范圍和C語言在64位機器中的情況一樣。在Java中,單字節數據類型稱為 byte,而不是char,而且沒有long long 數據類型。這些具體的要求都是為了保證無論在什么機器上,Java程序運行的表現都能完全一樣。
6、有符號和無符號數之間的轉換
在 信息的存儲和表示 這篇博客中我們講過計算機在解釋一個數據類型的值時主要有四個因素:位排列規則(大端或者小端)、起始位置、數據類型的字節數、數據類型的解釋方式。對於特定的系統來說,前兩種因素都是特定的,而對於后兩種因素的改變,則可以改變一個數據類型的值的最終計算結果,這就是強制類型轉換。
那么考慮相同整數類型的無符號編碼和補碼編碼,數據類型的大小是沒有任何變化的,變化的就是它們的解釋方式。比如1000這個二進制序列,如果用無符號編碼解釋的話就是表示8,而若采用補碼編碼解釋的話,則是表示-8。
①、有符號數強轉為無符號數
前面我們說過:無論是無符號編碼還是補碼編碼,其映射方式都是雙射,因此它們都一定存在逆映射。如果我們定義U2Bw(x)為B2Uw(x)的逆映射,則對於任意一個整數x,如果0 =< x < 2w,經過U2Bw(x)的計算之后,將得到唯一一個二進制序列。同樣的,如果我們定義T2Bw(x)為B2Tw(x)的逆映射,則對於任意一個整數x,如果-2w-1 =< x < 2w-1,經過T2Bw(x)的計算之后,也將得到唯一一個二進制序列。
可以很明顯的看出,對於0到2w-1-1這個區間內的整數來說,兩種編碼得到的二進制序列是一樣的。為了得到其它區間里的整數的映射關系,我們定義:
T2Uw(x) = B2Uw(T2Bw(x))
這個函數代表的含義是補碼編碼轉換為無符號編碼的時候,先將補碼編碼轉換為二進制序列,再將二進制序列轉換為無符號編碼,最終也就是補碼編碼轉為無符號編碼的計算。
下面我們簡單的推算一下上面的定義,究竟是如何轉換的,也就是有符號數 x 和與之對應的無符號數T2Uw(x) 的關系。我們將上面無符號編碼和補碼編碼的公式相減,
將0到w-2的位的加權和互相抵消),即 B2Uw(x) - B2Tw(x) = xw-12w-1 - (-xw-12w-1) = xw-12w
將等式左邊的B2Tw(x)移到等式右邊,即 B2Uw(x) = xw-12w + B2Tw(x)
此處我們令x為T2Bw(x),則 B2Uw(T2Bw(x)) = xw-12w + B2Tw(T2Bw(x)) = xw-12w + x
即 T2Uw(x) = xw-12w + x
此時考慮xw-1的情況,當xw-1為1時,也就是補碼編碼表示負數的時候,T2Uw(x)則為2w + x 。(此時x為負數,也就是說2w + x < 2w)
若xw-1為0時,則補碼編碼為正數,此時T2Uw(x) = x 。
綜上可知,有下列式子成立
從這個式子中可以很明顯的看出,最終得到的無符號數范圍為0 =< x < 2w。
下圖為表示補碼編碼與無符號編碼的對應關系,可以看出在0至2w-1-1之間,兩者是相等的,而其余區間則不同。
從上圖我們也可以得出:當將一個有符號數映射為它相應的無符號數時,負數就被轉換成了大的正數;而非負數會保持不變。
這里我們看一個小例子來理解一下:
#include <stdio.h> int main() { char t = 0xFF; unsigned char u = (unsigned char)t; //%d把對應的整數按有符號十進制輸出,%u把對應的整數按無符號十進制輸出 printf("t=%d,t2u=%u\n",t,u); return 0;//c標准規定建議main函數返回值為int }
輸出結果為:
這個結果怎么解釋呢,首先有符號char t=0xFFFF。這是因為C語言在64位系統中占用一個字節,轉換成二進制數即:1111 1111,轉換為補碼也是:1111 1111,我們套用下面補碼的公式可以得到:
1111 1111的值為 -1。然后根據我們上面的轉換公式:
可以得到轉換之后的值為 -1+28=255。也就是上面打印的結果。
②、無符號數轉換為有符號數
相反,我們用同樣的方式也可以證明從無符號編碼到補碼編碼的公式,我們依然將無符號編碼和補碼編碼的公式相減
即 B2Uw(u) - B2Tw(u) = uw-12w-1 - (-uw-12w-1) = uw-12w
即 B2Tw(u) = B2Uw(u) - uw-12w
此時我們令u為U2Bw(u),則 B2Tw(U2Bw(u)) = B2Uw(U2Bw(u)) - uw-12w = u - uw-12w
即 U2Tw(u) = u - uw-12w
此時考慮uw-1的情況,當uw-1為0時,也就是無符號編碼數值小於2w-1的時候,U2Tw(u)則為u 。
若uw-1為1時,也就是無符號編碼數值大於或等於2w-1的時候,此時U2Tw(u)= u - 2w。(此時U2Tw(u)為負數,因為 u < 2w)
綜上,我們可以得到無符號編碼轉換為補碼編碼的公式
同樣的,在0至2w-1-1之間,兩者依然是相等的,而其余區間則不同。
還是看一下下面的例子來理解:
#include <stdio.h> int main() { unsigned char u = 0xFF; char t = (char)u; //%d把對應的整數按有符號十進制輸出,%u把對應的整數按無符號十進制輸出 printf("u=%u,u2t=%d\n",u,t); return 0;//c標准規定建議main函數返回值為int }
輸出結果:
這應該很好理解了,無符號 0xFF,即1111 1111,采用的是無符號編碼,第一位不是符號位,那么轉換為十進制就是255,然后套用上面的公式:u-2w=255-28=-1
7、總結
本篇博客主要講解了有符號數和無符號數之間的轉換,我們需要明白它的原理,這篇博客也涉及到很多公式推導證明,LZ也是看了好幾遍才理解這些,大家如果第一遍看不懂也沒關系,多看幾遍,然后多用筆推導推導,還是不難理解的。下一章會介紹C語言中的有符號數和無符號數以及擴展和截斷數字。