刚开始做技术,接触的是硬件相关的编程,因此基本不用小数(浮点数)进行运算,也就没留意到计算机中小数“失精”存储的问题了。最近两年进行网络编程开发,也就是应用层开发了,工作上就接触到很多使用浮点数运算的场景,自然的,“失精”问题也就浮现了。
理清精确缺失问题(失精)
那么什么是”失精”问题?就是计算机是无法准确的存储大部分的十进制小数的。不信,可以运行以下C语言程序:
#include "stdio.h"
void main()
{
printf("%.7f", 10.2f-9.0f);
getchar();
}
结果输出:0.1999998 而不是0.2000000
既然知道了事实现象,我们就来研究下里面的真相。首先,我们将十进制的10.2用二进制表示:
1010.001100110011………无限小数 (不要问我怎么转的,自己回去补一下转换过程)
再者,将十进制9.0转化为二进制:1001(这个数小数部分为0,所以能够能用有限的数字表示)
好的,将两者相减,就得到:0.001100110011……… 因为计算机存储位数的限制,不可能存储无限位二进制数字,因此最终采用老办法– 截取高位存储,于是结果就是0.001100110011…(IEEE754标准存储24位有效数字),转换为十进制7位小数输出,就得到了0.1999998这个数字。
作为严谨的程序员,我们自然回想为什么十进制小数无法用二进制精确的表示呢?下面引入一个10cm长的杆子。
如果将1看做是这10cm的杆子,那么对于第一位小数,十进制的精度就是1cm,所以能准确的描述2/10的杆子长度4cm;但是二进制的精度是5cm,所以用二进制这个工具根本无法表示4cm,没法准确表示,怎么办呢?
那就无限逼近咯,于是就出现了课本上教我们的十进制转二进制的简易数学方法了。
好了,到此,就明白为什么0.2无法用二进制精确表示的了吧。
那么再总结下规律,十进制中的0.0-0.9这是个小数里面,出了0.0和0.5能被二进制准确表示外,其他都无法准确表示的。同理以2为基数的4进制8进制16进制都有和二进制相同的规律。
那么计算机程序是怎样处理这个问题的呢?
- 将浮点数用字符串存储起来,然后对字符进行十进制模拟运算,最后得出结果,这个方法在JAVA里面已经有现成的API。
- 对于比较成熟的语言,比如C,就会就运算之后,对结果进行进一步处理,如10.2-9.0=0.1999….,我们知道1位小数减去1位小数,得到的结果绝对不会超过1位小数。因此我们在运算前先记录最大的小数位数n,然后运算后,就对n+1位进行四舍五入的处理即可。
最后提醒一下,要注意一些不太成熟的语言如:php等脚本语言,例子如下
<?php
$f = 0.57;
echo (int)($f*100); //输出56而不是57
?>