簡單又複雜的“整數類型”

前言

因為一道題目讓我不斷地深追下去,挖出了我多年的噩夢——數據類型的範圍與長度。每次都想得頭痛,因為平臺不同、編譯器不同、編程語言不同等等因素,又沒去做實驗,網上那麼多說法該相信誰都不知道……那不如趁現在就來詳細地解決掉它吧。

一、原碼、反碼和補碼

基礎知識

相信在大學的《數字邏輯》課上都學過這個內容了,原碼、反碼和補碼都是基於二進制而言的:

【原碼】第1位表示符號位,其餘位是這個數的絕對值。這是最簡單能夠馬上想到的表示方式了。

【反碼】正數的反碼是其本身;負數的反碼:在原碼的基礎上,符號位不變,其餘位取反。

【補碼】正數的補碼是其本身;負數的補碼:在原碼的基礎上,符號位不變,其餘位取反,最後+1。

舉個例子,假設整數在機器上是用8位二進制數表示的(8位就和我們經常說的32位、64位是一樣的含義):

簡單又複雜的“整數類型”

為什麼要用原碼、反碼和補碼呢?

原碼的來源

為了讓二進制能夠表示負數,產生了原碼。

反碼的來源

一個正數和一個負數運算需要辨別符號位,然而單獨去辨別符號位會給電路設計帶來極大的複雜度,因此人們想只設計加法電路,讓符號位直接參與加法運算達到減法的目的,產生了反碼。例如:3-2 = 3+(-2) = [0000 0011]反+[1111 1101]反 = [0000 0001]反 = [0000 0001]原=1(注意反碼的加法當最高位進位的時候,最低位需要+1,不再詳細描述,參考百度百科《二進制反碼求和》)。這樣符號位就能夠參與運算了。

補碼的來源

反碼看起來很完美,但是仍然存在問題。例如3-3 = 3+(-3) = [0000 0011]反+[1111 1100]反 = [1111 1111]反 = [1000 0000]原=-0,而[0000 0000]反=[0000 0000]原 = +0,也就是說,零可以表示為兩種形式,這種歧義同樣不利於電路實現。並且由於反碼的加減法還需要對溢出位進行處理,於是產生了補碼。補碼對溢出位直接丟棄,而0的表示只有一種[0000 0000]補,[1000 0000]補則看成是-128,解決了所有問題。

原碼、反碼和補碼的範圍問題

值得注意的是,8位的原碼和反碼都只能表示[-127, +127]範圍內的整數,而補碼可以表示[-128, +127]範圍,多一個-128。這裡的-128是計算得到的,而不是從反碼推出的,-128根本無法用反碼錶示,卻能夠用補碼計算,比如-127+(-1) = [1000 0001]補+[1111 1111]補 = [1000 0000]補。所以我們經常背的整數取值範圍[-32768, +32767]之類的東西為什麼負數總比整數的真值大1,就是這樣來的。

計算機中按位取反會發生什麼?

既然計算機表示的時候用的是補碼,那麼如果對十進制的整數【按位取反】操作到底操作的是補碼還是二進制呢?

實驗一下吧:

printf("%d\n", ~(3));
printf("%d\n", ~(-3));

【平臺】windows 8 64位

【IDE】vs2013 32位

【語言】C語言

【取反操作】~

【取反結果】~3 = -4,~(-3) = 2

數值比較小,最高位沒有影響,就按照8位來仔細觀察第一組數據:

3 = [0000 0011]b = [0000 0011]原 = [0000 0011]反 = [0000 0011]補

-4 = [無法表示]b = [1000 0100]原 = [1111 1011]反 = [1111 1100]補

對補碼取的反,再來看第二組:

-3 = [無法表示]b = [1000 0011]原 = [1111 1100]反 = [1111 1101]補

2 = [0000 0010]b = [0000 0010]原 = [0000 0010]反 = [0000 0010]補

可以確信100%是對補碼取的反了,純的。

二、C語言中的整數類型的大小和範圍

以前我們常常會去記憶[-32767, +32768],尤其是在學pascal的時候,然而現在仔細想想,pascal都是多少年前的編程語言了,那時的電腦和現在的電腦完全不相同,記這個根本沒用。整數類型的大小和範圍和操作系統、編譯器、編程語言都息息相關,拋開運行環境談論sizeof出什麼結果的題目都是耍流氓,然而筆試題這種流氓經常存在………

整數類型的範圍與表示位數

用不同位數表示整數,取值的範圍就不相同,由於採用補碼,總可以多表示一個負數:

簡單又複雜的“整數類型”

無符號unsigned

無符號的時候,就可以不用擔心符號位了,也就是可以表示0~2^bit-1個數,比如:

簡單又複雜的“整數類型”

C語言中的整數類型及其長度

基本整數類型有:char、short int、int、long、long long(c99新增)。

我總是在死記長度,總以為long比int更長,但其實C語言標準是這樣規定的:

int最少16位(2字節),long不能比int短,short不能比int長,具體位長由編譯器開發商根據各種情況自己決定。

好一個“自己決定”……好一個“不能比”……還是通常情況吧,列個表:

簡單又複雜的“整數類型”

32位表示方式中,long int和int是一樣大的!同時還反映了一個問題:64位運行的代碼不一定能在32位上運行。

C語言數據類型名稱、輸出和編譯器的關係

g++和gcc

g++把.c和.cpp程序都認為是c++程序,gcc則會用C語言的方式編譯.c,用C++的方式編譯.cpp。也就是說,如果你用C寫的程序,用g++編譯,很可能會報語法錯誤,因為g++對語法要求更嚴格,儘管C++是C語言的超集。其他的區別就是,g++能夠自動鏈接c++的庫,而gcc需要手動設置參數。

gcc/g++與cl

vs使用的編譯器是cl.exe,這是微軟自己開發的編譯器。CL.exe是控制 Microsoft C 和 C++ 編譯器與鏈接器的 32 位工具。cl和clang是不同的,在Visual Studio 2015已經整合了clang編譯器,但它是被用於Android和 iOS上的應用開發。

整數類型不同表示方式以及輸出

Visual Studio是在windows下運行的,通常支持__intxx這種寫法來定義不同位數的整數,這是gcc/g++通常不支持的(沒有實驗過)。而long long這種寫法在Visual C++ 6.0上是不支持的(沒有實驗過)。不過,在Visual Studio 2013上,全部的寫法都支持,很可靠,列個表:

簡單又複雜的“整數類型”

並且,所有的printf寫法都支持:

簡單又複雜的“整數類型”

做個實驗:

【操作系統】windows 8 64位

【IDE】Microsoft Visual Studio 2013 32位

【編譯器】cl.exe win32

【代碼】

long long a = 1231321313131313131;
__int64 b = 1231321313131313131;

printf(" type=long long\n d=%d\n ld=%ld\n lld=%lld\n I64d=%I64d\n ---\n", a,a,a,a);
printf(" type=__int64\n d=%d\n ld=%ld\n lld=%lld\n I64d=%I64d\n", b,b,b,b);

【輸出結果】

簡單又複雜的“整數類型”

d和ld都溢出了,而lld、I64d可以工作得很好,而且對long long 和__int64沒有任何區別

整數類型越界會發生什麼?

這是一直都很好奇的事情,那就來實驗一下。

【操作系統】windows 8 64位

【IDE】Microsoft Visual Studio 2013 32位

【編譯器】cl.exe win32

【實驗結果】

  1. 取值範圍unsigned short int 0~65535unsigned int 0~4294967295int -2147483648~2147483647long -2147483648 ~ 2147483647long long -9223372036854775808 ~ 9223372036854775807超上限(越來越大)會從最小值開始重新增長:unsigned short int 65536=0 | 65537= 1unsigned int 4294967296=0 | 4294967297= 1int 2147483648=-2147483648 | 2147483649 = -2147483647long 2147483648=-2147483648 | 2147483649 = -2147483647long long 9223372036854775808 = -9223372036854775808 | 9223372036854775809 = -9223372036854775807超下限(越來越小)會從最大值開始重新減小:unsigned short int -1=65535 | -2=65534unsigned int -1=4294967295 | -2=4294967294int -2147483649=2147483647 | -2147483650=2147483646long -2147483649=2147483647 | -2147483650=2147483646long long -9223372036854775809 = -9223372036854775807 | 9223372036854775810 = -9223372036854775806

【探究原因】

想一下剛才的補碼,假設32位,int取最大值2147483647,打開你的計算器,選擇查看→程序員,輸入這個數字,看到它的補碼:

簡單又複雜的“整數類型”

[0111 1111 1111 1111 1111 1111 1111 1111]補 + [0000 0000 0000 0000 0000 0000 0000 0001]補的結果是[1000 0000 0000 0000 0000 0000 0000 0000]補 = -2147483648。

這就是為什麼越界的2147483648,打印輸出-2147483648的原因了。

【其他】

注意如果你直接進行賦值:

int a = -2147483648;

VS是會報錯的:

簡單又複雜的“整數類型”

long long 也是如此,因此這時候應該用:

int a = INT_MIN;
long long b = LLONG_MIN;

來表示,可以看到它們的宏定義:

簡單又複雜的“整數類型”

說好的可以多表示一個負數呢,怎麼不行了呢,具體原因參考wiki《VS編寫C程序報錯error C4146: 一元負運算符應用於無符號類型,結果仍為無符號類》

三、JAVA語言中的整數類型的大小和範圍

基本信息

因為我在儘量主學Java副學Python,所以這裡也記錄一下java的整數類型。java的整數類型比較神奇,有四種基本整數類型:byte、short、int、long,但由於java的設計初衷是跨平臺運行的,Write Once and Run Anywhere,所以這幾種類型的字長都是固定的,與任何其他的32位64位都無關,列個表:

簡單又複雜的“整數類型”

你可以自己測試一下:

System.out.println("Byte: " + Byte.SIZE/8);
System.out.println("Short: " + Short.SIZE/8);
System.out.println("Integer: " + Integer.SIZE/8);
System.out.println("Long: " + Long.SIZE/8);

java中的unsigned類型

java是幾乎沒有unsigned類型的。為什麼說幾乎呢,因為在多年的呼籲之後,最新的jdk8支持了unsigned的靜態方法調用(也就是說不支持直接寫unsigned int這種寫法,只能通過Integer.xxxx來調用),參看《Unsigned Integer Arithmetic API now in JDK 8》。真應了那句老話:真香!為什麼大家那麼希望有unsigned類型呢?因為常常需要處理圖片,而我們知道通常的圖片數據是從0變化到255的,如果有unsigned byte,那不就剛好了嘛~由於沒有unsigned,目前主流的做法是使用更大的類型比如short或者int。值得注意的是,如果要把表達0~255取值的byte轉換到short/int,要處理一下符號。因為當從0~255的short/int轉換為byte時,考慮他們的補碼,例如255:

255 = short [0000 0000 1111 1111]補 → byte [1111 1111]補 = -1

128 = short [0000 0000 1000 0000]補 → byte[1000 0000]補 = -128

0 = short [0000 0000 0000 0000]補 → byte[0000 0000]補 = 0

127 = short[0000 0000 0111 1111]補 → byte[0111 1111]補 = 127

可以看出,0~127(short)被映射到0~127(byte),而128~255則被映射到(-128~-1)了,因此在byte轉回short/int時,如果不加處理,得到的值會是-128:

-128 = byte[1000 0000]補 → short [1111 1111 1000 0000]補 = -128

處理的方法很簡單,加個掩碼0xff屏蔽掉高位的符號擴展即可,也就是將byte的值與0xff進行按位與:

-128 & 0xff = byte[1000 0000]補 & [1111 1111] → short[1111 1111 1000 0000]補 & [0000 0000 1111 1111] = [0000 0000 1000 0000]補 = 128

得到的值就正常了,用代碼實驗一下:

short s_init = 128,s_force,s_and;
byte b_force;
b_force = (byte)s_init;
s_force = (short)b_force;
s_and = (short)(b_force & 0xFF);
System.out.println("初始short值= "+s_init+"\n轉為byte= "+b_force+"\nbyte轉為short= "+s_force+"\nbyte掩碼後轉為short= "+s_and);

得到的結果是:

初始short值= 128
轉為byte= -128
byte轉為short= -128
byte掩碼後轉為short= 128

java中的char

char類型長度2個字節,而且取值是無符號的0~65535,其他編程語言通常都是1個字節。java的char是Unicode編碼,可以存放中文字符。那麼為什麼不用它來作為unsigned int 用呢?

【原因1】輸出為字符。

java的char類型是設計為存儲unicode字符的,採用UTF-16固定寬度的編碼格式。雖然賦值的是數值88,但當調用System.out.println(a);的時候,出現的是字母X。

【原因2】運算困難。

char a = 88;
a = a + 1;

編譯器會報錯需要char類型,而給的是int,因為當char類型運算後就是int類型了,不能直接存回char類型,需要進行強制轉換:

a = (char)(a + 1);

既然這麼麻煩,為何不直接用int呢?

java中整數類型越界會發生什麼?

和C語言是一樣的:當越上界,會從最小值繼續累加;當越下界,會從最大值繼續減小。原因同樣是因為補碼溢出位被丟棄,在測試的時候,不能直接賦值越界數值,否則會提示類型不匹配或者整數太大了。使用常量+1再強制轉換類型,達到越界目的。

byte a = (byte)(Byte.MAX_VALUE+1);

輸出結果:-128

ps:碼字不易,如果你覺得有幫助的話,幫忙轉發一下吧~

---------------------------------

微信公眾號:輪子工廠,機器學習 | 數據結構與算法 | 源碼分析 | 資源與工具分享


分享到:


相關文章: