整數與浮點數

整數

在寫數字的時候,表示負數很簡單,就加個負號就好了。那在電腦中該怎麼表示負數?

最簡單的方式是用最高位元來表示正負號,0 代表正,1 代表負,像是如果用 8 個位元來表示一個數, 就是 1000 0101 就是 1000 0011,但這樣會發生一個問題,就是表示 有兩種方式:不管最高位是 10,只要其他位都是 0,這樣它就是表示 ,所以勢必需要另一個方法來有效利用空間。

補數和二補數

先用一個不那麼位元的想法來想,有 位元的二進位數字,介於 ,如果分一半給非負整數、另一半給負整數,有一個很簡單的方法,就是對於 的數,都分配給負數, 表示的負數就是和它模 下同餘、最大的那個負數 ,也就是

這樣做有一個好處,就是既然我們是在模 下做事,代表它可以套用所有模除的性質,我們只要把數字對到 的範圍裡就好。既然在電腦裡減法不方便,就都先換成用來表示負數的那個數,再相加就好了,非常方便。

回到位元的看法, 的補數定義為 ,也就是將一個二進位數字的 0、1 互換。例如:
0000 0001 的補數是 1111 1110
0101 1010 的補數是 1010 0101
在 C++ 中,~ 這個運算子就是用來做這件事情。

的二補數的定義是 ,也就是補數再加上 1,注意我們還是在模 下做事,也就是如果進位到超出範圍,就把超出的部分捨去,這樣就得到了一個數的二補數,例如:
0000 0001 的補數是 1111 1110,二補數是 1111 1111
0101 1010 的補數是 1010 0101,二補數是 1010 0110
0000 0000 的補數是 1111 1111加 1 後是 1 0000 0000,二補數是 0000 0000
1000 0000 的補數是 0111 1111,二補數是 1000 0000

這就是目前最常見的電腦儲存有號(正負)整數的方式,這裡說的二補數的字面上的值,就是我們剛剛說的用同餘得出來的數字。

C++ 中常見的整數型態範圍是這樣:
unsigned 開頭的是無號的,也就是只有 和正數,因此 位元的無號整數最大值是

型態 byte 數 最大整數 最小整數
char 1 127 -127
int 4 2147483647 -2147483648
long long 8 9223372036854775807 -9223372036854775808
unsigned int 4 4294967295 0
unsigned long long 8 18446744073709551615 0

建議大概記一下這些的範圍,像是 int 的最大值大約是 long long 的最大值大約是

接下來,有個特殊狀況,就是有兩個數取二補數後會跟原本一樣,這兩個數分別是 和最小整數。 的相反數本來就是 也沒有人跟它同餘;最小整數是 ,但表示的數字範圍只到

結論:

  • 位元可以表示的有號數範圍為
  • 位元可以表示的無號數範圍為
  • 位元的同個二進位值所表示的有號數 和無號數 滿足
  • 和最小整數取二補數後不變
  • 位元二進位值 的二補數是

大數運算

事實上所有整數進位制都可以套用類似二補數的規則, 進位制 位數可以用的數字有 ,如果 是偶數,可以和二進位一樣,非負整數和負數各一半,表示範圍是 ;奇數的話,則是 0 給 0、剩下正負數各分一半,表示範圍是 。這樣的話可以保證 表示的數一定是 表示的數的相反數,或是等於

這可以幹嘛呢?就是大數運算啦,以前大數運算還要多寫減法,現在只要寫加法就夠了!算 的方法很簡單, 就是每一位都是 位數字,減去 再把 1 加回去就好了。

浮點數

會存整數之後我們也要存小數,我們會用一種叫浮點數的東西來存它。用來規定浮點數表示方式的標準叫 IEEE 754,大部分程式語言都會按照這個標準。

浮點數是用類似科學記號的方式來儲存,只是換成二進位而已。用來儲存一個浮點數的記憶體由高位(左)到低位(右)被分成三段: - 符號位,只佔一個位元,0 表示此數為正,1 表示此數為負。 - 指數域,實際表示的值是指數域儲存的值減去指數偏移量 位指數域的偏移量是 。 - 小數域,表示一個介於 的小數,因為我們知道這個小數的個位數字是 ,這也是唯一在小數點前的數字,所以不需要儲存它,因此這裡只儲存小數點後的位數

若指數域所表示的指數值是 ,而小數域表示的值是 ,那麼這個浮點數是: 至於是正或負,依符號位而定。並且

注意到負指數不是用二補數表示,而是減去指數偏移量。若指數偏移量是 ,且指數域有 個位元,則可表示的指數範圍是 ,至於 被用作特殊用途。

為小數域的長度是有限的,但小數有無限小數,無限小數中又有循環小數和無理數,因此會發生不精確的問題,造成計算和判斷上的錯誤,遇到這種狀況有這幾種辦法: - 盡量不要用到小數,需要做運算的話,如果不需要做到太多難做的操作(例如乘法就是很簡單的操作),可以先用分子分母分開存的方式來計算,要輸出的時候再換成小數就好。 - 判斷一個浮點數是否等於一個值的時候,允許一個誤差範圍(相差小於一個極小的數),例如:

1
2
double a = /*...*/;
if(a == 0){/*...*/}
改成
1
2
double a = /*...*/;
if(fabs(a - 0) < 1e-9){/*...*/}

因為浮點數利用科學計號的方式儲存一個數,因此它除了可以表示分數以外,也可以表示一個極大的整數。

以下是 C++ 常見的浮點數型態,注意 double 的有效位數並不比 long long 來得長。

資料型態 指數域長度(bit) 指數偏移量 小數域長度(bit)
float 8 127 23
double 11 1023 52

特殊值

前面有提到當 位指數域的指數偏移量是 時, 用於表示特殊值,而特殊值如下:

零與負零

指數為 的浮點數,符號位為正表示 ,為負則為 。因為一般小數域只儲存小數點後的位數,也就是自動默認小數點前有一個 ,因此 這個數字必須要用特殊值來存。至於正零跟負零唯一的差別在於負零輸出的時候會有負號,把負數一直除以超大數字就會變成 -0,例如 -1 / 1e200 / 1e200,不過比較的時候是不會出事的。

正負無窮

指數為 ,小數域為 0,符號位為正表示正無窮,為負表示負無窮,需要它的話,在 C++ 中可以用 INFINITY 來得到,它在 math.h 裡面,只是在競程上沒啥用途,頂多就是除以 會出現無窮,可以幫你 debug。

NaN

Not a Number 的縮寫,指數為 ,小數域不是 0,在反三角函數丟一些超出定義域的數字時會跑出這東西。

黑科技

有兩個很大的型態叫 __int128long double,前者是 128 位元整數,可以存到大約 的整數,在運算過程會溢位的時候很有用,不過 Windows 不能用,還有沒有內建輸入輸出,所以需要的話你可以自己寫,或是輸入輸出不超過 long long 範圍的話,也可以用 long long 輸入輸出,再轉過來或轉過去。

至於 long double 是一個非標準的浮點數,通常有 80 位元,指數域 15 位、小數域 64 位,也就是說它可以不失任何精度地存下 long long 範圍內的數字。既然它是非標準的東西,就代表它實際長怎樣依編譯器而定。它可以在 Windows 用、可以輸入輸出,可以當一般的浮點數型態用。