• 负数在计算机中是怎么存储


    今天,发生一件非常有趣的事情。

    公司同事问了我一个问题:为什么 2.0 - 1.1 = 0.89999999 呢?不应该是 0.9吗?

    原来是,他问了周围一圈的同事,都给他的是同一个回答,说这是精度问题。他百思不得其解,怎么就会产生精度问题呢。再问,就没人知道原因了。

    然后,我就看到了他抱着一本厚厚的书在看。拿过来一看,是一本Java书,厚厚的六百多页,这还仅是第一卷。哟呵,这是准备大干一场啊。

    看在他这么努力学习的份上,还有他那对知识极度渴望的眼神。我决定,把我毕生所学传授与他。

    于是,就给他详细讲解了,计算机中是怎么存储一个数的,十进制是怎么在转二进制的过程中丢失精度的,以及浮点数是怎么遵循IEEE 754 规范的,在浮点数进行加减运算的过程中会经历对阶、移位运算等过程,以及在此过程中是怎么丢失精度的。(这些问题在之前的文章中都有解答,参看“为什么0.1+0.2=0.30000000000000004”)

    然后,成功的把他彻底搞懵逼了。怎么这么难啊。

    原来,他的计算机基础比我还匮乏,不知道什么是位运算,不知道什么是原码、反码和补码。

    本着我的热心肠,我就给他普及了一下这些知识 ---- 负数的补码形式和位移运算。

    我们知道,一个数分为有符号和无符号。对于,有符号的数来说,最高位代表符号位,即最高位1代表负数,0代表正数。

    在计算机中,存储一个数的时候,都是以补码的形式存储的。而正数和负数的补码表示方式是不一样的。正数的补码就等于它的原码,而负数的补码是原码除符号位以外都取反,然后 + 1 得来的。以一个int类型为例(4个字节即32位)

    14的原码为:

    0000 0000 0000 0000 0000 0000 0000 1110
    

    它的反码、补码和原码都是一样的。

    -14的原码为:

    //最高位1为符号位,代表此数为负数
    1000 0000 0000 0000 0000 0000 0000 1110
    

    反码为原码除了符号位以外的其他位都取反(即0变为1,1变为0),

    1111 1111 1111 1111 1111 1111 1111 0001
    

    补码为反码 + 1 ,注意二进制中是满二进一。

    1111 1111 1111 1111 1111 1111 1111 0010
    

    位的左移,右移运算就是分别向左和向右移动N位。移位的规则是:

    1. 不管有没有符号位,左移都是在低位补0
    2. 带符号右移,是在高位补符号位,即正数补0,负数补1
    3. 无符号右移,无论该数是正数还是负数都在高位补0

    因左移就在右边低位补0就可以了,比较简单,我就以负数的右移来举例,是怎么计算无符号右移和带符号右移的。还是以 -14 为例。

    // -14的补码
    1111 1111 1111 1111 1111 1111 1111 0010
    // 带符号右移用 >> 表示,即右移一位 -14>>1,高位补符号位1,低位舍去
    1111 1111 1111 1111 1111 1111 1111 1001
    // 无符号右移用 >>> 表示,即右移一位 -14>>>1,最高位补0
    0111 1111 1111 1111 1111 1111 1111 1001
    

    我们可以通过程序来验证一下 -14>>1和 -14>>>1的结果是否正确。

    1. -14>>1 = -7

    //我们算出来 -14>>1的补码为:
    1111 1111 1111 1111 1111 1111 1111 1001
    //那它具体代表的数值是多少呢?
    //首先,补码 -1 得到反码
    1111 1111 1111 1111 1111 1111 1111 1000
    //然后,反码取反得到原码,最高位符号位不变
    1000 0000 0000 0000 0000 0000 0000 0111
    

    这结果不就是 -7 吗,然后通过程序计算一下结果:

    public class TestMove {
        public static void main(String[] args) {
            System.out.println( -14>>1);
        }
    }
    

    结果同样也是-7 。说明了我们位移操作没问题。

    2. -14>>>1=2147483641

    我们通过一段程序去验证:

    package com.test.binary;
    ​
    /**
     * @Author zwb
     * @DATE 2019/12/3 15:49
     */
    public class TestBinary {
        public static void main(String[] args) {
            //我们自己计算出来的 -14>>>1 结果
            String bin = "01111111111111111111111111111001";
            double res = binToDec(bin);
            System.out.println(res);
            //通过计算机计算的结果
            System.out.println(-14>>>1);
        }
    ​
        //二进制转为十进制
        public static double binToDec(String bin){
            int index = bin.indexOf(".");
            int len = bin.length();
            double res = 0;
            //index为-1说明没有小数
            if(index == -1){
                for(int i = 0; i< len; i++){
                    res += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(len-1-i)));
                }
            }else{
                //整数部分
                int partA = 0;
                for(int i = 0; i< index; i++){
                    partA += Math.pow(2,i) * Integer.parseInt(String.valueOf(bin.charAt(index-1-i)));
                }
                //小数部分
                double partB = 0;
                for(int j = index + 1; j < len; j++){
                    partB += Math.pow(2,index - j) * Integer.parseInt(String.valueOf(bin.charAt(j)));
                }
                res = partA + partB;
            }
            return res;
        }
    }
    

    运行之后的结果,可以在控制台打印看到:
    file

    上边第一个是我们自己通过推算它的补码,然后通过二进制转十进制的一个算法算出来的最终结果,第二个就是直接通过位运算算出来的结果。可以看到结果是一模一样的。

    至此,是不是对原码,反码,补码以及位运算左移右移,有了比较清晰的认识了呢?

  • 相关阅读:
    PHP Context学习系列《十》
    学习php记录《九》
    学习php记录《八》
    php学习记录《七》
    换到新工作后
    学习php记录《六》
    学习php记录《五》
    学习php记录《四》
    学习php记录《三》
    html基础
  • 原文地址:https://www.cnblogs.com/starry-skys/p/11991906.html
Copyright © 2020-2023  润新知