1. 为什么负数要用补码表示?
⼗进制数转⼆进制 采用除 2 取余法。例如:数字8转⼆进制的过程如下图:
整数类型的数字 在计算机中的存储方式,就是将十进制数 转换成⼆进制。
以int 类型的数字为例,int类型大小为32位,其中最高位作为 "符号标志位" (正数的符号位是0,负数的符号位是1),剩余的31位 表示二进制数据。例如:int类型的数字1 的⼆进制数表示如下图:
负数在计算机中 用"补码"表示,补码就是将正数的二进制 全部取反再加1。例如:int类型的数字-1 的⼆进制数表示如下图:
问题:为什么计算机要用补码的⽅式来表示负数?
先假设不用补码的方式 来表示负数,而只是将最高位的符号标志位变为1 来表示负数,如下图:
-2+1 应该等于 -1,但上面的运算结果却是-3。所以这种负数的表示方式 不能用常规的加法来计算,因此需要先进行特殊处理。要先判断数字 是否为负数,如果是负数 就要将加法操作变为减法操作 才能得到正确的结果。
如果负数不使用 补码的方式表示,则做基本的加减法运算时,还需要多一步操作 来判断是否为负数。如果为负数,则需要将加法操作变为减法操作 或者将减法操作变为加法操作,这样效率将会很低,因为加减法运算在计算机中的使用 是非常频繁的,所以应该尽量 简化运算过程。
如果负数使用 补码的方式表示,则负数的加减法操作 实际上与正数的加减法操作 是一样的。例如:如果负数用补码的方式表示,则运算-2+1的过程如下图:
如果负数不使用 补码的方式表示,则做基本的加减法运算时,还需要多一步操作 来判断是否为负数。如果为负数,则需要将加法操作变为减法操作 或者将减法操作变为加法操作,这样效率将会很低,因为加减法运算在计算机中的使用 是非常频繁的,所以应该尽量 简化运算过程。
如果负数使用 补码的方式表示,则负数的加减法操作 实际上与正数的加减法操作 是一样的。例如:如果负数用补码的方式表示,则运算-2+1的过程如下图:
2. 十进制小数与二进制的转换
已经知道了 整数十进制转⼆进制,小数部分的转换 采用的是乘 2 取整法。例如:数字8.625转⼆进制的过程如下图:
最后将 "整数部分" 和 "小数部分" 结合在⼀起,其结果就是1000.101。
但是,并不是所有小数都可以用二进制表示。例如:数字0.1转⼆进制的过程如下图:
可以发现,0.1的⼆进制表示 是无限循环的。由于计算机的资源是有限的,所以只能用 "近似值" 来表示0.1 (即 在有限的精度条件下,最接近0.1的二进制数),因此会造成 精度的缺失。
注:二进制小数转十进制时,小数点后的指数幂 是负数。比如:二进制1010.101转十进制的过程如下图:
3. 计算机是怎么存小数的?
⼆进制小数1000.101 为"定点数形式" (即小数点是定死的,不能移动,如果小数点移动 则原数值就会被改变)。然而,计算机存储小数采用的是 "浮点数形式" (即小数点是浮动的)。
示例:1000.101这个⼆进制数,可以表示成 1.000101 x 2^3,类似于数学上的科学记数法。
注:如果⼆进制使用科学记数法,则需要规范化。不仅要保证 基数为2,还要保证 小数点左侧只有1位,并且必须为1。
所以⼆进制数1000.101 规格化表示成1.000101 x 2^3。其中:
注:如果⼆进制使用科学记数法,则需要规范化。不仅要保证 基数为2,还要保证 小数点左侧只有1位,并且必须为1。
所以⼆进制数1000.101 规格化表示成1.000101 x 2^3。其中:
- 000101 为尾数部分,即小数点后面的数字;
- 3 为指数部分,指定了小数点在原数据中的位置;
现在的绝大多数计算机 使用的浮点数,⼀般采用的是 IEEE 制定的国际标准。这种标准形式如下图:
- 符号位:0 表示正数, 1 表示负数;
- 指数位:指定了小数点在原数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长 则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,即小数部分 (比如:二进制1.0011 x 2^(-2)的尾数部分 就是0011),尾数的长度决定了 这个数的精度,因此如果想要表示 精度更高的小数,就要提高 尾数位的长度;
- double 的尾数部分是 52位,float 的尾数部分是 23位,由于同时都带有⼀个 固定隐含位(小数点左边的整数1),所以double有 53个⼆进制有效位、float有 24个⼆进制有效位。double的精度用十进制表示为 log10(2^53) 约等于 15.95,float的精度用十进制表示为 log10(2^24) 约等于 7.22。因此,double的有效数字是 15~16位,float的有效数字是 7~8位,这些有效位包含了 整数部分和小数部分。
- double的指数部分是 11位,float的指数位是 8位,意味着double相比float 能表示更大的数值范围。
移动后的二进制浮点数1.010101 x 2^3 的小数点左侧的有效位1 没有被存储。因为根据IEEE标准规定:⼆进制浮点数的小数点左侧 只能有1位,且该位上只能是1。所以既然这⼀位 永远都是1,就不需要存起来了。因此,23位尾数只存储小数部分,可以节约1位的空间,尾数就能多存一位小数,精度就更高一点。
float的⼆进制浮点数 转换成十进制时,要考虑到这个隐含的1。转换公式如下图:
举例:将下图中的float 转换成十进制:
4. 0.1 + 0.2 == 0.3 ?
0.1 的float浮点数:
可以看到,8位指数部分是 01111011,23位的尾数部分是 10011001100110011001101。尾数部分中有 0011循环,但由于尾数是有 长度限制的,所以最终只会显示一个近似值。
0.2 的float浮点数:
可以看到,8位指数部分是 01111100 ,23位的尾数部分是10011001100110011001101,尾数部分和0.1相同,也是⼀个近似值。
0.1⼆进制浮点数 转换成十进制的结果是 0.100000001490116119384765625 :
0.2的⼆进制浮点数 转换成十进制的结果是 0.20000000298023223876953125 :
这两个数 相加的结果是 0.300000004470348358154296875 :
可以看到,在计算机中 0.1 + 0.2 并不等于完整的 0.3。这是因为 有的小数无法用 "完整" 的二进制来表示,所以计算机只能采用 近似数的方式来保存,近似数相加的结果 也是一个近似数。
二进制只能精准表达 2除尽的数字。如:1/2、1/4、1/8;但对于0.1(1/10)、0.2(1/5),则在⼆进制中 无法精确表示,因此需要 根据精度舍入。(十进制也有无法除尽的数,如:1/3、1/7 也需要根据精度舍入)