计算机里为什么不能精确表示浮点数
计算机的世界 是 一个二进制的世界。
我们先来看看 十进制 和 二进制的相互转换。
十进制 --> 二进制:对整数部分,就是除2取余,倒着来。 对小数部分,就是乘2取整,正着来。
先来从宏观角度看看十进制和二进制间的关系
先来看整数部分,比如一个 8 bit 的大小:
(二进制 --> 十进制)
0000 0001 --> 1
0000 0010 --> 2
0000 0100 --> 4
0000 1000 --> 8
………………
我们可以用这些,拼成一个 十进制的数值。
比如 十进制的 12 , 可以看成 8 + 4 ,
所以对应二进制: 0000 1000 + 0000 0100 = 0000 1100
类似的,对整数部分,十进制中的全部整数是可以用二进制来表示的,
因为有个最小的1,最慢的也就是可以用 1 来累加。
再来看小数部分:
(二进制 —> 十进制)
0.1 —> 1 / 2 = 0.5
0.01 —> 1 / 4 = 0.25
0.001 —> 1 / 8 = 0.125
0.0001 —> 1 / 16 = 0.0625
0.0000 1 —> 1 / 32 = 0.03125
………………
仍然的,我们可以用这些,拼成一个十进制的小数。
比如: 十进制的 0.75 ,可以看成 0.5 + 0.25
所以对应二进制: 0.1 + 0.01 = 0.11
但是这样问题就来了, 在拼十进制的 0.2 时,就要哇哇的哭出来了。。。。
十进制的 0.2 , 二进制的拼凑过程:
1 / 4 ,0.25 太大。
1 / 8 , 0.125 太小。接着往下凑。
1 / 8 + 1 / 16 = 0.125 + 0.0625 = 0.1875, 还是小,接着往下一位加。
1 / 8 + 1 / 16 + 1 / 32
= 0.1875 + 0.03125 = 0.21875, 大了,不加这一位,加下一位。
1 / 8 + 1 / 16 + 1 / 64
= 0.1875 + 0.015625 = 0.203125 , 还是大了,不加这一位,加下一位。
1 / 8 + 1 / 16 + 1 / 128
= 0.1875 + 0.0078125 = 0.1953125,小了,还需要接着往下加。
………………
你就会发现,这样无限往下拼凑,二进制永远不能精确表示十进制的 0.2 。 多多少少会有一点点的误差。
这些误差对应我们日常使用来说是可以忽略的,但是对于银行、金融这样对精度有严格要求的,可能就会出问题。
现在我知道的,就有 2 种解决方法。
Java 里的 BigDecimal 类。(原理跟第二种方法也是一样的)
干脆直接不存浮点数类型了,直接存储整数,和小数部分有几位 这 2 个数据。
用的时候再拿出来进行相应的缩放。
这样二进制不能精确表示小数,实际上,二进制表示十进制小数就是不精确的。
感觉就有点类似于 十进制中, 小数也不能精确表示分数。
比如 分数的 1 / 3 , 小数就要表示成 0.33333333, 3 无限循环。。。
从微观角度看看二进制
这些小数, 在计算机里是怎么存储 二进制的呢?
有 2 种思路, 定点数形式、和 浮点数形式。
定点数:
例如现在是 一个 32 bit 的计算机,
可以这么划分:
也可以这么划分:
因为整数部分 和 小数部分的位数都是固定的,所以叫做定点数的表示方法。
这些部分怎么划分的,也就会影响到他们的表示范围 和 表示精度。
浮点数:
利用 类似科学计数法 的形式,达到了让小数点浮动的效果。
比如 : 1.234 * 10^4 。 1.234 是尾数。 10 是基数。 4 是指数。
IEEE754 标准就是规定这些规则的一个约束。 于 1985 年 intel 公司 和 加州伯克利分校的教授制定。
IEEE 754 规定的基本形式是:
( + or - ) 1.(mantissa) * 2 ^ (exponent) 。 mantissa 表示尾数。 exponent 表示指数。
对单精度浮点型 float 来说,32 bit, 符号位 1 bit , 指数 8 bit, 尾数 23 bit。
小数点可以往前移,也可以往后移。
所以指数里的 8bit,也是需要表示正负数,就一半一半,127表示0,0-126 表示负数,128-255表示正数。
尾数里的 23bit,表示的是小数部分,省去了整数部分的数值 1 。
对双精度浮点型 double 来说, 64bit, 符号位 1bit, 指数 11 bit, 尾数 52 bit。
下面就以代码的形式来验证下这个规则:
上述验证过程的代码:
int i = Float.floatToRawIntBits(0.2f);
String s = Integer.toBinaryString(i);
System.out.println("0.2 的二进制表示形式: " + s);
System.out.println("length : " + s.length());
// 0 01111100 10011001100110011001101
int exponent = Integer.parseInt("01111100", 2);
int mantissa = Integer.parseInt("10011001100110011001101", 2);
System.out.println("指数 exponent 的十进制: " + exponent);
System.out.println("尾数 mantissa 的十进制: " + mantissa);
// 所以最终的表示形式为 : + 1.10011001100110011001101 * 2 ^ (-3)
// 最后的二进制为 : 0. 00110011001100110011001101
// 以下这个过程,是看看移动完后的二进制结果对应着的十进制。 要看累加的过程。
String mStr = "00110011001100110011001101";
int[] index = new int[32]; // 下标值就表示,这个 1 对应着是第几位数。
for (int j = 0; j < mStr.length(); j++) {
if (mStr.charAt(j) == '1') {
index[j + 1] = 1;
}
}
double res = 0.0;
System.out.println("``````````````````````````````````````````````````````");
for (int j = 0; j < index.length; j++) {
// bit 位上是 1 , 说明有他表示的数,加上去。
if(index[j] == 1){
double temp = 2;
for (int k = 1; k < j; k++) {
temp = temp * 2;
}
temp = 1 / temp;
System.out.print("temp = " + temp + " ; ");
res = res + temp;
System.out.println("res = " + res);
}
}
System.out.println("``````````````````````````````````````````````````````");
System.out.println("final res : " + res);
本文地址:https://blog.csdn.net/weixin_43201538/article/details/107433982