浮点数精度问题及解决方案

误差的原因

来看下面这个例子:

#include <stdio.h>

int main()
{
    float a = 0.1;
    
    printf("%f\n",a);
    printf("%.10f\n",a);
    printf("%.15f\n\n",a);
    
    float b = 100000000;
    float c = 5;
    float d = b + c;
    printf("%f\n\n", d);
    
    return 0;
}

输出为

0.100000 		//正确输出是因为四舍五入了
0.1000000015		//将精确度提高就会发现误差
0.100000001490116	//再次提高精度

100000008.000000	//加法居然算错了?傻了吧?

第一种造成精度误差的原因是大多数小数转化为二进制为无限小数,而计算机内存有限,只能保存一部分,例如:
0.1转换为二进制是0.0001100110011001100110011001100...
float只能保留一部分,所以一定会有误差,好在大多数场景用不到这么高的精度,不过要注意多个小误差累积成大误差。

第二种造成精度误差的原因是因为较大的浮点数与较小的浮点数相加。例子中100000000 + 5居然等于100000008,这是因为浮点数是用科学计数法来存储数据,即:
100000000 = 1.011111010111100001 \times 2^{26}
5 = 1.01 \times 2^{2}
两个数相加的时候先要对阶,就是把指数部分统一,按照小阶数化为大阶数的原则:
5 = 0.00000000000000000000000101 \times 2^{26}
按照IEEE 754标准,浮点数float为32位,1位数符,8位阶码,23位尾数。
而0.00000000000000000000000101(小数点后有26位)不能用23位保存下来,所以舍入成0.00000000000000000000001(小数点后有23位),就是这里的舍入造成的误差。
对阶后,将尾数相加得到结果:
(0.00000000000000000000001+1.011111010111100001)\times 2^{26} = 100000008

货币的存储方案

浮点数在工程控制,游戏引擎等众多领域有着广泛的应用。但是因为其有误差,所以不适合在金融领域使用。

某水果3.6元一斤,某人买了1.2斤,总计4.32元。如果用float存储价格的话,一单的误差比较小看不出差别。但是如果统计一年的营业额,累加很多float就会出现较大的误差。

货币通常只精确到小数点后两位,所以我们可以将其乘100,用整数存储。上面那个例子可以将3.6存成int型360,1.2用float存储,4.32存成int型432(其实432由浮点数计算而来也是有误差的,但是一次计算误差非常小,四舍五入就可以得到正确的值)。累加一年营业额时累加的是int,所以不会有误差。

更特殊的情况,某水果3.6元一斤,某人买了1.2斤,75折,含税0.02,快递费1.5,总价为3.6x1.2x0.75x0.98+1.5,多次使用浮点数参与运算,有可能造成累积误差。这种情况可以可以分子和分母分别保存。计算时只计算分子,计算的结果也分子分母分别保存,只有显示时才除一下分母,而一次计算的误差非常小,四舍五入就可以得到正确的值。不过用这种方法要小心分子和分母溢出,或者写到一半发现这里要开个平方根或者求个三角函数,然后发现结果根本不是有理数了,进而开始怀疑人生。

posted @ 2020-08-09 15:39:54
评论加载中...
发表评论