浮点数为什么不能准确表示

浮点数为什么不能精确表示

1. 表示问题

问题的根本原因并不是我们找不到一种二进制表示浮点数的方法,而是计算机只能使用有限的二进制位,而某些实数的精确二进制表示需要无限位,因此不可能用有限位数精确表示所有实数。

例如十进制的:

\[0.5=0.1_2 \\ 0.25=0.01_2 \\ 0.125=0.001_2 \]

这些小数都能够被二进制精确表示。

但十进制的 0.1:

\[0.1_{10}=0.00011001100110011\ldots_2 \]

其二进制表示是无限循环的。

那无论我们怎么设计浮点数表示方法,都不可能存储无限个二进制比特位。

也即,无论设计什么样的固定长度编码,都无法精确表示所有实数。

这不是 IEEE 754 浮点数标准设计得不够好,而是有限存储空间的数学限制。

因此,改变表示方法可以改变哪些数字能够被精确表示,却无法让所有实数都被精确表示。

2. 位数限制

还有一种情况,即使一个数能够用有限二进制精确表示,浮点数也不一定能精确存储它。

在常见的 IEEE 754 标准中:

类型 总位数 符号位 指数字段 小数字段
float 32 1 8 23
double 64 1 11 52

注意:对于正规化的二进制浮点数,最高有效位通常隐含为 1,因此:

  • float 有 24 位二进制有效精度。
  • double 有 53 位二进制有效精度。

这就意味着,虽然浮点数可以表示非常大或非常小的数,但用于表示有效数字的位数是有限的。

例如:

float a = 16777217.0f;

16777217 是整数,它的二进制表示当然是有限的。

但 float 只有 24 位有效精度,无法精确表示这个数字。

因此它会被舍入为 16777216。

下面代码输出 “yes”:

float a = 16777217.0f;
if(a == 16777216.0f) cout << "yes" << endl;

3. 运算误差

前面分析,浮点数精度受两个因素影响:

  1. 表示问题:有些实数的二进制表示需要无限位,例如 0.1。
  2. 位数限制:即使二进制表示是有限的,也可能超过浮点数能够保留的有效位数。

此外,运算本身也可能产生舍入误差。

最经典的例子就是:

#include <iostream>
using namespace std;

int main() {
    double a = 0.1;
    double b = 0.2;

    cout << (a + b == 0.3) << endl;

    return 0;
}

在常见的 IEEE 754 双精度环境下,输出:

0

为什么?

因为:

\[0.1+0.2\neq0.3 \]

这里说的不相等,是指计算机内部存储和运算得到的浮点值不完全相等,而不是数学上的不相等。

在这里,0.1 转换成二进制后是一个无限循环小数,由于计算机的存储空间有限,不可能保存无限位数字,只能截取并舍入。

因此:

double a = 0.1;

实际保存的是一个非常接近 0.1 的值,而不是数学意义上精确的 0.1。

同样,0.2 和 0.3 也无法使用有限位二进制精确表示。

所以在进行运算时:

0.1 + 0.2

结果可能出现微小误差。

例如:

#include <iostream>
#include <iomanip>
using namespace std;

int main() {
    double a = 0.1;
    double b = 0.2;

    cout << setprecision(17) << a + b << endl;
}

典型输出:

0.30000000000000004

这就是浮点数精度误差的来源之一。

为什么浮点数表示范围大,精度反而可能更低

浮点数牺牲了部分精度,换取了更大的数值表示范围。

我们比较一下:

类型 常见大小 数值范围 精度特点
int 4 字节 约 ±21 亿 范围内所有整数精确
float 4 字节 约 ±3.4×10³⁸ 约 7 位十进制有效数字
double 8 字节 约 ±1.8×10³⁰⁸ 约 15~16 位十进制有效数字
long long 8 字节 约 ±9.22×10¹⁸ 范围内所有整数精确

float 虽然可以表示比 int 大得多的数,但它不能精确表示 int 能表示的所有整数。

不过,double 的精度高于 32 位 int,能够精确表示其全部整数取值。因此,不能笼统地说所有浮点类型的精度都低于所有整数类型。

为什么浮点数越大,精度越容易损失

这里再介绍一个重要概念:

浮点数的精度通常是相对精度,而不是固定的绝对精度。

例如:

#include <iostream>
using namespace std;

int main() {
    float a = 100000000.0f;

    cout << (a + 1.0f == a) << endl;
}

输出:

1

也就是说:

\[100000000+1=100000000 \]

从数学上看显然不正确。

但对于 float,这是正常现象。

原因是:

浮点数在数值较小时,相邻两个可表示数之间的距离比较小。

随着数值增大,相邻两个可表示数之间的距离也会增大。

例如,IEEE 754 单精度浮点数:

数值附近 相邻可表示浮点数的间距
1 (2^{-23}\approx0.0000001192)
1024 (2^{-13}=0.0001220703125)
16777216 2(向更大数的方向)
100000000 8

因此,在 (100000000) 附近,float 能区分的相邻数字之间相差 8。

这时再加上 1:

float a = 100000000.0f;
a = a + 1.0f;

由于 1 太小,计算结果会被舍入回原值。

所以:

\[a+1=a \]

这也是为什么数值计算中,大数与小数相加时可能损失小数部分。


浮点数越大,绝对精度越容易损失,根本原因确实与指数有关,但更准确地说,是因为浮点数的有效数字位数固定,而指数增大后,相邻可表示数之间的间距也随之增大。

对于正规化的 float,可以写成:

\[x=(1.b_1b_2\cdots b_{23})_2\times2^E \]

其有效数字的最后一位代表:

\[2^{-23}\times2^E \]

因此,在同一个指数区间内,相邻浮点数之间的间距为:

\[\Delta x=2^{E-23} \]

这就是解释浮点数精度变化最关键的公式。

情况1:指数 E = 0

此时:

\[\Delta x=2^{-23} \]

即:

\[\Delta x\approx0.0000001192 \]

也就是说,在 1 附近,float 可以区分非常接近的数。

情况2:指数 E = 10

此时:

\[\Delta x=2^{10-23}=2^{-13} \]

即:

\[\Delta x=0.0001220703125 \]

在 1024 附近,相邻浮点数之间的距离已经明显增大。

情况3:指数 E = 24

此时:

\[\Delta x=2^{24-23}=2 \]

这意味着:

在 (2^{24}=16777216) 附近,float 已经不能区分相差 1 的部分整数了。

C++ 中浮点数应该如何比较?

由于浮点数可能存在舍入误差,通常不建议直接使用 == 比较经过计算的浮点数。

例如:

double a = 0.1 + 0.2;
double b = 0.3;

if (a == b) {
    cout << "相等";
}

可能无法得到预期结果。

更常见的做法是使用误差容限:

#include <iostream>
#include <cmath>
using namespace std;

int main() {
    double a = 0.1 + 0.2;
    double b = 0.3;

    double eps = 1e-9;

    if (abs(a - b) < eps) {
        cout << "相等" << endl;
    }

    return 0;
}

输出:

相等

这里:

\[|a-b|<\varepsilon \]

意味着两个数的误差足够小,可以认为它们近似相等。

需要注意,1e-9 并不是通用标准。实际开发中,应根据数值大小和应用所允许的误差选择合适的容限,必要时结合相对误差进行判断。

posted @ 2026-11-04 19:58  光風霽月  阅读(7)  评论(0)    收藏  举报