首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計疑難解答

C++ 浮點數比較陷阱:遞減到 0 不停止的原因與正確比較方法

     編輯:程式設計疑難解答

問題代碼現象


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-172.77556 × 10⁻¹⁷0.0000000000000000277556接近零的正誤差
-5.55112e-17-5.55112 × 10⁻¹⁷-0.0000000000000000555112接近零的負誤差
1.11022e-161.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;
}

避坑最佳實踐

  1. ❌ 永遠不要 用 ==、!= 直接比較浮點數
  2. ✅ 循環終止條件用 fabs(val - target) < EPS
  3. ✅ 金融/貨幣計算**強制使用整數(分/厘)或 BigDecimal**
  4. ✅ 科學計算採用 std::numeric_limits::epsilon() 為基準
  5. ✅ 輸出調試時用 std::setprecision(17) 顯示完整精度

語言對比

語言推薦比較方式備註
C/C++自定義 EPS / ULP無內建約等運算符
JavaMath.abs(a-b) < EPS / Double.compareBigDecimal 必用於貨幣
Pythonmath.isclose(a, b, rel_tol=1e-9)3.5+ 內建 isclose
C#Math.Abs(a-b) < EPSdecimal 類型適合貨幣
JavaScriptMath.abs(a-b) < Number.EPSILONES6 Number.EPSILON
Gomath.Abs(a-b) < EPS無標準庫函數
Rust(a-b).abs() < EPS / approx cratetrait 約束泛型實現

經典面試題變體


// 輸出什麼?
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 次後變負數,進入無限循環
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved