计算机的世界 是 一个二进制的世界。

我们先来看看 十进制 和 二进制的相互转换。

十进制 --> 二进制:对整数部分,就是除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 种解决方法。

  1. Java 里的 BigDecimal 类。(原理跟第二种方法也是一样的)

  2. 干脆直接不存浮点数类型了,直接存储整数,和小数部分有几位 这 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);