C++筆記-浮點數

浮點數

名言:「算錢用浮點,遲早被人扁。」

浮點數是一個很難理解的東西,

這裡特別拿出來說明。

宣告

如果遇到一定要用浮點數的情況,請使用 doublelong double 的型態,

不要使用 float

不要使用 float

不要使用 float

IEEE 754 與浮點數誤差

現在還沒有完整的方式可以存一個實數,目前電腦的儲存方式是採用 IEEE 754 標準

IEEE 754

IEEE754是自20世紀80年代以來現代電腦最廣泛使用的浮點數運算標準,除了浮點數的表示以外,它還定義了關於負0、反常值以及其運算子與例外情況,像是inf(Infinite)、nan(Not A Number)這些特殊數值。

我們要先知道如何做進制轉換, 欲將十進位轉成二進位,對於整數部分,就是一直除以2直到商數為0,再依序由下往上取出餘數:

而小數則相反,需要一直乘以2直到變成0為止,且每次的運算都只取小數部分,再依序由上往下取出整數。

假設要轉換 432.1 為二進制,

計算結果餘數
432/22160
216/21080
108/2540
54/2270
27/2131
13/261
6/230
3/211
1/201

由下往上寫結果就是 110110000

對小數部分

計算結果整數部分
0.1*20.20
0.2*20.40
0.4*20.80
0.8*21.61
0.6*21.21
0.2*20.40
0.4*20.80
0.8*21.61
0.6*21.21

這時你會發現它是無限循環小數,結果為 $0.0\overline{0011}$

最後整數小數合併就是 $110110000.0\overline{0011}$


要如何表示單精度浮點數?

以 $12.625_{10}$ 這個浮點數為例,

  1. 不管正負號,將 $12.625_10$ 拆成 $12+0.625$
  2. 分別寫成二進位,即 $1100 + 0.101 = 1100.101$
  3. 正規化,得到 $1100.101 = 1.100101 \times 2^3$,整數部分不可為0
  4. Sign 佔 1 格,因為是正數,Sign為 0,如果是負數就是 1 => 0
  5. Exponent 佔 8 格,次方數為 3,請加上 127 後轉成 8 位數二進位 => 10000010
  6. Fraction(mantissa) 佔 23 格,把小數部分,也就是 101 填進去,如果不到 23 位,需要將後面補 0 直到滿足 23 位 => 10100000000000000000000

至此我們就完成了 IEEE754 單精度浮點數的轉換,需要注意的是,如果正規化後小數部分超過 23 位(無法整除),就要直接放棄掉多的部分。

我們可以看更多範例:


以 $8.5$ 為例,

  1. 不管正負號,將 $8.5$ 拆成 $8 + 0.5$
  2. 分別寫成二進位, $8 + 0.5 = 2^3 + 2^-1 = 1000 + 0.1 = 1000.1$
  3. 正規化後得到 $1000.1 = 1.0001 * 2^3$
  4. 因為是正的,Sign填 $0$
  5. 次方數為 $3$,加上 $127$ 後填入轉成二進位填入Exponent => $10000010$
  6. 把0001填入Fraction(mantissa)並補滿23位,得到 $00010000000000000000000$

以 $1.1$ 為例,

  1. 不管正負號,將 $1.1$ 拆成 $1 + 0.1$
  2. 分別寫成二進位,注意看你會發現這個數在二進位下除不盡, $1 + 0.1 = 2^0 + 2^{-4} + 2^{-5} + 2^{-8} + 2^{-9} + 2^{-12} + 2^{-13} + 2^{-16} + 2^{-17} + 2^{-20} + 2^{-21} + 2^{-23} + … $ $ = 1 + 0.00011001100110011001101 = 1.00011001100110011001101$
  3. 正規化後得 $1.00011001100110011001101 = 1.00011001100110011001101 * 2^0$
  4. 正的 Sign 為0
  5. 次方數為 0,加 127 後二進位為 $01111111$
  6. 剩下的Mantissa填進去,得到 $00011001100110011001101$

以 $-3$ 為例,

  1. 不管正負號,將 $3$ 拆成 $3+0.0$
  2. 分別寫成二進位,$11.0$
  3. 正規化後得 $1.1 * 2^1$
  4. 負的 Sign 為 1
  5. 次方數為 1,加上 127 後二進位為 $10000000$
  6. Mantissa為 $10000000000000000000000$

強制轉型

可以將型別用小括號刮起來將後面的運算子強制轉型

cout << (double)5/3 << "\n";

四捨五入

有時會遇到需要將答案四捨五入到第 $k$ 位,使用 <iomanip> 控制。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <iomanip>
using namespace std;
int main(){
  double pi = 3.141592653589;
  
  cout << "Original: ";
  cout << pi << "\n";
  
  cout << "Fixed: ";
  cout << fixed << pi << "\n"; // 以固定位數輸出浮點數
  cout << pi << "\n";
  
  cout << "Set precision to 0: ";
  cout << setprecision(0) << pi << "\n"; // setprecision 指定輸出幾位數
  cout << pi << "\n";
  
  cout << "Set precision to 3: ";
  cout << setprecision(3) << pi << "\n";
  cout << pi << "\n";
  
  int k = 5;
  cout << "Set precision to k = 5: ";
  cout << setprecision(k) << pi << "\n";
  cout << pi << "\n";

}

做完後可觀察到,設定完輸出幾位數後會持續生效直到再次設定。

誤差應對

轉型

將浮點數轉成整數時由於是截斷取整,5.0 可能存成 4.999...,轉成整數時會被轉成 4

為了解決這個問題,可以將它先加上一個極小值再做轉型,這個極小值通常是 $10^{-5}$ 到 $10^{-10}$,視需求決定。

1
const double EPS = 1e-6;

宣告一個常數 double,值為 $10^{-6}$。

1
cout << (int)(5.0 + EPS) ;

比較

直接使用 == 比較浮點數是一件危險的事,因為它們不一定會被精確保存,近似值不能保證它們以相同誤差保存。

解決的辦法是:如果它們很接近就當相等,也就是判斷它們的絕對值是否小於極小值。

載入數學函式庫 <cmath>,並使用 fabs() 得到浮點數的絕對值

1
2
3
double a = 1.213;
double b = 3.639/3;
cout << (fabs(a - b) < EPS);