浮点数为什么不能准确表示
浮点数为什么不能精确表示
1. 表示问题
问题的根本原因并不是我们找不到一种二进制表示浮点数的方法,而是计算机只能使用有限的二进制位,而某些实数的精确二进制表示需要无限位,因此不可能用有限位数精确表示所有实数。
例如十进制的:
这些小数都能够被二进制精确表示。
但十进制的 0.1:
其二进制表示是无限循环的。
那无论我们怎么设计浮点数表示方法,都不可能存储无限个二进制比特位。
也即,无论设计什么样的固定长度编码,都无法精确表示所有实数。
这不是 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. 运算误差
前面分析,浮点数精度受两个因素影响:
- 表示问题:有些实数的二进制表示需要无限位,例如 0.1。
- 位数限制:即使二进制表示是有限的,也可能超过浮点数能够保留的有效位数。
此外,运算本身也可能产生舍入误差。
最经典的例子就是:
#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 转换成二进制后是一个无限循环小数,由于计算机的存储空间有限,不可能保存无限位数字,只能截取并舍入。
因此:
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
也就是说:
从数学上看显然不正确。
但对于 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 太小,计算结果会被舍入回原值。
所以:
这也是为什么数值计算中,大数与小数相加时可能损失小数部分。
浮点数越大,绝对精度越容易损失,根本原因确实与指数有关,但更准确地说,是因为浮点数的有效数字位数固定,而指数增大后,相邻可表示数之间的间距也随之增大。
对于正规化的 float,可以写成:
其有效数字的最后一位代表:
因此,在同一个指数区间内,相邻浮点数之间的间距为:
这就是解释浮点数精度变化最关键的公式。
情况1:指数 E = 0
此时:
即:
也就是说,在 1 附近,float 可以区分非常接近的数。
情况2:指数 E = 10
此时:
即:
在 1024 附近,相邻浮点数之间的距离已经明显增大。
情况3:指数 E = 24
此时:
这意味着:
在 (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;
}
输出:
相等
这里:
意味着两个数的误差足够小,可以认为它们近似相等。
需要注意,1e-9 并不是通用标准。实际开发中,应根据数值大小和应用所允许的误差选择合适的容限,必要时结合相对误差进行判断。

浙公网安备 33010602011771号