← 返回首页
从一个例子深入理解计算机的二进制
发表时间:2023-05-19 01:43:19
从一个例子深入理解计算机的二进制

计算机科学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构。

1.冯诺依曼体系结构

我们今天从一个例子展开,深入理解计算机处理的数据和指令一律用二进制数表示。

2.实例

public static void main(String[] args) {
     System.out.println(0.1+0.2);
     System.out.println(0.5-0.2);
}

执行结果:

0.30000000000000004
0.3

根据冯诺依曼体系结构说法,计算处理和存储数据最终都是以二进制数表示,但是并不是所有的小数都可以精确的转换为二进制,准确地说,绝大多数小数都不能精确的转换为二进制。

0.1转化成二进制的算法:

0.1*2=0.2======取出整数部分0 ------ 余0.2
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
接下来会无限循环
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
所以0.1转化成二进制是:0.0001 1001 1001 1001......

同理,0.2转换为二进制的算法:

0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
接下来会无限循环
0.2*2=0.4======取出整数部分0 ------ 余0.4
0.4*2=0.8======取出整数部分0 ------ 余0.8
0.8*2=1.6======取出整数部分1 ------ 余0.6
0.6*2=1.2======取出整数部分1 ------ 余0.2
所以0.2转化成二进制是:0.0011 0011 0011 0011......

既然,0.1和0.2都不能用二进制精确表示,那么0.3也不能精确用二进制表示。

先说结论:在计算机的世界里,压根就不存在0.3这个小数,只能找到无数多个无限接近于0.3的小数。

这里,也许有学生会问:为什么0.5-02的输出结果,就是精确的0.3呢?我的回答是:你看到0.3仅仅是个假象,是计算机最终近似出来的结果,不是真正意义的0.3。

我的结论是:只要这个小数本身不能用二进制精确表示,那么你看到的这个精确的小数,就不是真正的小数,而是计算机近似出来的结果。

之所以0.5-0.2 能得到0.3 的结果,只能说这次减法运算的运气比较好,减完后的结果,恰好能近似转换为0.3而已。

我们来看下面例子:

public static void main(String[] args) {
      System.out.println(0.2 + 0.4);
      System.out.println(0.8 - 0.2);
      System.out.println("-------------------------");
      System.out.println(0.3 + 0.1);
      System.out.println(0.7 - 0.3);
}

执行结果:

0.6000000000000001
0.6000000000000001
-------------------------
0.4
0.39999999999999997

因为0.6和0.4也不能精确用二进制表示,所以我们使用加法和减法测试时,究竟是加法能近似为0.6或者0.4,还是减法能近似为0.6或者0.4也是不确定的。但是我要说的是:无论怎么近似,其实在计算机的世界里,压根不存在0.4和0.6,你看到的仅仅是假象,是个近似的结果。

当然:有极个别小数是可以用二进制精确表示的,比如:0.5,0.25,0.125 等等。当然,我说这番话的主要目前是想告诉你,即便在数学上两个小数运算出来的结果是0.5,也不能代表计算机计算出来的结果一定是0.5。

我们来看下面的例子:

public static void main(String[] args) {
    System.out.println(0.5 + 0.25);
    System.out.println(0.5 - 0.25);
    System.out.println("--------------------");
    System.out.println(0.8 - 0.3);
    System.out.println(0.7 - 0.2);
}

执行结果:

0.75
0.25
--------------------
0.5
0.49999999999999994

因为,0.5和0.25都可以用二进制精确表示,所以0.5和0.25之间无论加减运算都可以得到精确的小数。

0.5虽然本身可以用二进制精确表示,但是0.5如果仅仅是另外两个小数的计算结果,由于另外两个小数本身并不能精确用二进制表示,所以也不能保证计算结果就是精确的0.5。

下来咱们进一步解释第一个案例中为什么0.2+0.1不能近似为0.3,而0.5-0.2可以近似为0.3,其实无论0.2+0.1还是0.5-0.2都不能得到精确的0.3,之所以0.2+0.1=0.30000000000000004, 是因为这次加法运算转换后的误差更大,所以无法近似为0.3了,如果我们使用更高精度的BigDecimal来表示0.2和0.1那么转换后的误差就极小,那么0.2+0.1和0.5-0.2都可以近似转换为0.3了,我们通过代码证明如下:

public static void main(String[] args) {
     //System.out.println("Hello world!");
     System.out.println(0.1+0.2);
     System.out.println(0.5-0.2);

     System.out.println("-------------使用BigDecimal---------------");
     BigDecimal bigDecimal1 = new BigDecimal(String.valueOf(0.1));
     BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(0.2));
     System.out.println(bigDecimal1.add(bigDecimal2));

     bigDecimal1 = new BigDecimal(String.valueOf(0.5));
     bigDecimal2 = new BigDecimal(String.valueOf(0.2));
     System.out.println(bigDecimal1.subtract(bigDecimal2));

}

执行结果:

0.30000000000000004
0.3
-------------使用BigDecimal---------------
0.3
0.3

结论:

计算机所有数据本质都是使用二进制表示,因此绝大多数小数都无法精确转换为二进制。至于控制台输出显示出的精确小数都是计算机近似后的结果。