問題代碼現象
void decrement(double& val) {
while (val != 0.0) { // 危險!浮點數直接比較相等
val -= 0.1;
cout << val << endl;
}
}
// 輸出:... 0.3, 0.2, 0.1, 2.77556e-17, -0.1, -0.2 ... 無限循環
// 出現科學計數法數值 (如 2.77556e-17) 即為精度誤差證據
根本原因:IEEE 754 浮點數表示局限
// 0.1 在二進制中無法精確表示(無限循環小數)
// 0.1₁₀ = 0.0001100110011...₂
// double 存儲 53 位有效數字,必須截斷/捨入
// 實際存儲值 ≈ 0.1000000000000000055511151231257827021181583404541015625
// 累積 10 次 0.1 減法:
// 理論:1.0 - 10×0.1 = 0.0
// 實際:1.0 - 10×0.10000000000000000555... = -5.55e-17 ≠ 0.0
科學計數法數值含義
| 輸出值 | 科學計數法 | 十進制近似值 | 含義 |
| 2.77556e-17 | 2.77556 × 10⁻¹⁷ | 0.0000000000000000277556 | 接近零的正誤差 |
| -5.55112e-17 | -5.55112 × 10⁻¹⁷ | -0.0000000000000000555112 | 接近零的負誤差 |
| 1.11022e-16 | 1.11022 × 10⁻¹⁶ | 0.000000000000000111022 | 典型 double epsilon 量級 |
正確比較方法
方法一:絕對誤差閾值(最常用)
const double EPS = 1e-9; // 根據業務精度調整
bool isZero(double val) {
return fabs(val) < EPS; // 絕對值小於閾值視為零
}
bool doubleEquals(double a, double b) {
return fabs(a - b) < EPS;
}
// 修正遞減循環
while (!isZero(val)) {
val -= 0.1;
if (val < 0) val = 0; // 防止負值
}
方法二:相對誤差閾值(大數值適用)
bool doubleEqualsRelative(double a, double b, double relEps = 1e-9) {
double diff = fabs(a - b);
double maxAbs = max(fabs(a), fabs(b));
return diff <= maxAbs * relEps;
}
方法三:ULP 比較(專業級,處理全範圍)
#include
#include
bool almostEqualULP(double a, double b, int maxULP = 4) {
// 處理符號、NaN、無窮大
if (a == b) return true; // 包含 +0 == -0
if (isnan(a) || isnan(b)) return false;
if (isinf(a) || isinf(b)) return false;
// 轉換為整數比較(利用 IEEE 754 佈局)
int64_t aInt = *reinterpret_cast(&a);
int64_t bInt = *reinterpret_cast(&b);
// 使負數在整數序中也遞增
if (aInt < 0) aInt = INT64_MIN - aInt;
if (bInt < 0) bInt = INT64_MIN - bInt;
return llabs(aInt - bInt) <= maxULP;
}
避坑最佳實踐
- ❌ 永遠不要 用
==、!= 直接比較浮點數
- ✅ 循環終止條件用
fabs(val - target) < EPS
- ✅ 金融/貨幣計算**強制使用整數(分/厘)或 BigDecimal**
- ✅ 科學計算採用
std::numeric_limits::epsilon() 為基準
- ✅ 輸出調試時用
std::setprecision(17) 顯示完整精度
語言對比
| 語言 | 推薦比較方式 | 備註 |
| C/C++ | 自定義 EPS / ULP | 無內建約等運算符 |
| Java | Math.abs(a-b) < EPS / Double.compare | BigDecimal 必用於貨幣 |
| Python | math.isclose(a, b, rel_tol=1e-9) | 3.5+ 內建 isclose |
| C# | Math.Abs(a-b) < EPS | decimal 類型適合貨幣 |
| JavaScript | Math.abs(a-b) < Number.EPSILON | ES6 Number.EPSILON |
| Go | math.Abs(a-b) < EPS | 無標準庫函數 |
| Rust | (a-b).abs() < EPS / approx crate | trait 約束泛型實現 |
經典面試題變體
// 輸出什麼?
float f = 0.1f;
double d = 0.1;
cout << (f == d); // false!float 精度更低,擴展為 double 時誤差不同
// 循環幾次?
for (float f = 1.0f; f != 0.0f; f -= 0.1f) { count++; }
// 通常 10-11 次後變負數,進入無限循環