十進制浮點數轉二進制涉及整數部分「除二取餘」與小數部分「乙二取整」的雙重算法,且受限於 IEEE 754 浮點數表示精度,轉換結果往往存在誤差。本文深度解析轉換原理、C++ 實現、精度誤差根源、IEEE 754 位級佈局、定點數替代方案與現代標準庫工具。
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
→ 讀取餘數自下而上:1101₂
設小數 f ∈ [0, 1),重複:
f × 2 = 整數部分 + 新小數部分
取整數部分作為二進制位 (0 或 1)
用新小數部分繼續,直到小數部分為 0 或達到精度上限
0.8125 × 2 = 1.625 → 整數 1,新小數 0.625
0.625 × 2 = 1.25 → 整數 1,新小數 0.25
0.25 × 2 = 0.5 → 整數 0,新小數 0.5
0.5 × 2 = 1.0 → 整數 1,新小數 0.0 結束
→ 讀取整數自上而下:1101
驗證:1×2⁻¹ + 1×2⁻² + 0×2⁻³ + 1×2⁻⁴ = 0.5 + 0.25 + 0 + 0.0625 = 0.8125 ✓
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0 (循環開始)
→ 0.0001100110011...₂ (無限循環)
符號位: 0
指數位: 01111011 (123 - 127 = -4)
尾數位: 10011001100110011001101 (隱含最高位 1,共 24 位有效數字)
實際存儲值 ≈ 0.10000000149011612
相對誤差 ≈ 1.49e-7
#include <iostream>
#include <vector>
#include <string>
#include <cmath>
#include <limits>
#include <iomanip>
std::string intToBinary(int n) {
if (n == 0) return "0";
bool neg = n < 0;
unsigned int val = neg ? -static_cast<unsigned int>(n) : n;
std::string bin;
while (val > 0) {
bin.push_back('0' + (val & 1));
val >>= 1;
}
if (neg) bin.push_back('-'); // 簡化符號處理
std::reverse(bin.begin(), bin.end());
return bin;
}
std::string fracToBinary(double frac, int maxBits = 52) { // double 尾數 52 位
std::string bin;
const double eps = 1e-15;
for (int i = 0; i < maxBits && frac > eps; ++i) {
frac *= 2;
if (frac >= 1.0) {
bin.push_back('1');
frac -= 1.0;
} else {
bin.push_back('0');
}
}
return bin.empty() ? "0" : bin;
}
std::string floatToBinary(double value, int maxFracBits = 52) {
if (std::isnan(value)) return "NaN";
if (std::isinf(value)) return value > 0 ? "+Inf" : "-Inf";
if (value == 0.0) return "0.0";
bool neg = value < 0;
double absVal = neg ? -value : value;
// 分離整數與小數部分
double intPart;
double fracPart = std::modf(absVal, &intPart);
long long intVal = static_cast<long long>(intPart);
std::string result;
if (neg) result += '-';
result += intToBinary(intVal);
result += '.';
result += fracToBinary(fracPart, maxFracBits);
return result;
}
int main() {
std::cout << std::fixed << std::setprecision(17);
std::vector<double> testValues = {
0.8125, 0.1, 0.5, 0.25, 0.75,
13.625, -5.125, 0.0, 1.0
};
for (double v : testValues) {
std::string bin = floatToBinary(v, 60);
// 驗證:解析回十進制
double parsed = std::stod("0" + bin.substr(bin.find('.'))); // 簡化驗證
std::cout << v << " → " << bin << " (誤差: " << std::abs(v - parsed) << ")\n";
}
// IEEE 754 位級檢視
float f = 0.1f;
uint32_t bits = *reinterpret_cast<uint32_t*>(&f);
std::cout << "\n0.1f IEEE 754: " << std::bitset<32>(bits) << '\n';
return 0;
}
| 符號 (1) | 指數 (8) | 尾數 (23) |
| S | E7-E0 | M22-M0 |
值 = (-1)^S × 1.M × 2^(E-127) (正規化數, E ∈ [1,254])
特殊值: E=0,M=0 → ±0; E=0,M≠0 → 非正規化數; E=255,M=0 → ±Inf; E=255,M≠0 → NaN
| 符號 (1) | 指數 (11) | 尾數 (52) |
值 = (-1)^S × 1.M × 2^(E-1023) (正規化數, E ∈ [1,2046])
| 特性 | 單精度 (float) | 雙精度 (double) |
|---|---|---|
| 有效位數 | ~7 位十進制數字 (24 bits) | ~15-16 位 (53 bits) |
| 指數範圍 | ~10⁻³⁸ 到 10³⁸ | ~10⁻³⁰⁸ 到 10³⁰⁸ |
| 機器 epsilon | 2⁻²³ ≈ 1.19e-7 | 2⁻⁵² ≈ 2.22e-16 |
| 0.1 表示誤差 | ~1.49e-7 | ~1.39e-17 |
// Q16.16 定點數:16 位整數 + 16 位小數
using Fixed16_16 = int32_t; // 或 int64_t 防溢出
constexpr int FRAC_BITS = 16;
constexpr int SCALE = 1 << FRAC_BITS;
Fixed16_16 toFixed(double f) { return static_cast<Fixed16_16>(f * SCALE); }
double toDouble(Fixed16_16 f) { return f / static_cast<double>(SCALE); }
// 運算
Fixed16_16 add(Fixed16_16 a, Fixed16_16 b) { return a + b; }
Fixed16_16 mul(Fixed16_16 a, Fixed16_16 b) {
return static_cast<Fixed16_16>((int64_t)a * b >> FRAC_BITS);
}
// C++23 提案 P1841: std::decimal 型別
// 精確表示十進制小數,無二進制轉換誤差
// #include <decimal>
// std::decimal::decimal64 price = 0.1_df; // 精確 0.1
// price += 0.2_df; // 精確 0.3
bool approxEqual(double a, double b, double eps = 1e-9) {
return std::abs(a - b) <= eps * std::max({1.0, std::abs(a), std::abs(b)});
}
// 或 ULP 比較 (更精確)
#include <cmath>
int ulpDistance(double a, double b) {
// 實現略...
}
double kahanSum(const std::vector<double>& v) {
double sum = 0.0, c = 0.0;
for (double x : v) {
double y = x - c;
double t = sum + y;
c = (t - sum) - y;
sum = t;
}
return sum;
}
#include <charconv>
#include <array>
std::string doubleToString(double v) {
std::array<char, 64> buf;
auto result = std::to_chars(buf.data(), buf.data() + buf.size(), v);
return std::string(buf.data(), result.ptr);
}
#include <format>
std::cout << std::format("{:.17e}\n", 0.1); // 科學記數法 17 位
std::cout << std::format("{:.52b}\n", 0.1); // 二進制輸出 (假設支援)
std::cout << std::format("0x{:a}\n", 0.1); // 十六進制浮點數
template <typename T>
void printIEEE754(T value) {
static_assert(std::is_floating_point_v<T>);
using UInt = std::conditional_t<sizeof(T)==4, uint32_t, uint64_t>;
UInt bits = *reinterpret_cast<UInt*>(&value);
std::cout << std::bitset<sizeof(T)*8>(bits) << '\n';
}
std::abs(a-b) <= epsfrac *= 2; bit = frac >= 1; frac -= bit;十進制浮點數轉二進制的核心矛盾在於:**十進制有限小數 ↔ 二進制無限循環小數** 的表示不匹配。
工程鐵律:
std::bitset 位級輸出、std::format 十六進制浮點數理解浮點數轉換的數學本質、硬體表示限制與工程規避策略,是從事數值計算、圖形學、金融系統、嵌入式開發、科學計算等領域開發者的必修內功。