为何整型范围中负数比正数多一个?

1.问题

如图所示,整型范围中,负数均比正数多一个?

2.解决方案

引用博客链接:https://juejin.cn/post/7128196204655018014

2.1引子

所有的负数范围都比整数多 1 个数字,其实这是计算机的存储和加减运算机制决定的。

  1. 首先,计算的存储只有 0 和 1,每个位置要么存 0,要么存 1,这些位置又叫做位(即 bit)。
  2. 其次,拿 byte 举例,它在目前计算的标准中是 8 位的,也就是说:1 byte = 8 bit;所以一个 byte 在计算机中只有 8 个可以存放 0 和 1 的位置,8 个位置放上 0 或 1,穷举的全部可能性为 2 的 8 次方,即 2^8=256。
    所以,在一个 byte 中最多只能表示 256 个不同意义的事物(可以是任何可能的事物),在这里如果是数字的话,就只能有 256 个数字了。如果我们不需要用它表示负数,那么它可以表示 0至255 这 256 个数字;如果它需要标识正负,计算机中会用高位表示符号,其他位表示数字,而 0 表示正号,1 表示负号。这时候 0 开头的二进制数字表示的范围是 0至127,而 1 开头的二进制数字表示的范围是 -0至-127,所以被正负号占用一位后,实质只能表示 -127至127 这 255 个数字而不是 256 个数字。
  3. 但是,0 和 -0 本质上是没什么区别的,如果能够将 -0 改为代表其他数字,那么表示的范围就能增加,所以就有了:-0 表示 -1、-1 表示 -2、以此类推到 -127 表示 -128 这样的情况。

虽然上述可以增加范围,但实际中不会有人为了增加一个数字范围而改变这种人类不容易理解的表达方式的,之所以实际情况确实如此表示,是有深层原因的:计算机人员为了简化计算机的计算过程和提高效率,不想让计算机先判断数字的正负,再使用加法或减法来计算,而是简化成:计算机只需要进行加法操作,并且忽略正负号的识别。

2.2 引入反码(将减法变为加法)

在反码计算时,我们希望将符号位也带入计算,这样原来的负数便会
要达到这个目的的理论很简单,就是 1-2 可以表示为 1+(-2) 的,还是用 byte 举例,

第一步:1-2=-1  这种适合人类思维的计算过程是:00,000,001 - 00,000,010 = 10,000,001(十进制为 - 1)

这里是识别正负号再人工计算的,这对于计算机来说不够简单,效率也不高;

第二步:1-2 改为:1+(-2)=-1 后,其计算过程为:00,000,001 + 10,000,010 = 10,000,011(十进制为 - 3)

显然这个计算过程是不正确的,为了能够直接做加法,还需要对负数取反后计算,即将负数除了符号位,其他位全部取反,10,000,010 取反后就是 11,111,101—— 这种取反后的二进制码也叫反码,取反前的二进制码则叫做原码。(顺便说明下反码的转换过程:正数的反码是其本身,负数的反码是除了符号位外其他位全部取反;反码转正码只需要逆转此过程)

第三步:将原码的计算过程取反后的计算过程为:

(原)00,000,001 + (原)10,000,010 = (原)10,000,011(十进制为 - 3)

(反)00,000,001 + (反)11,111,101 = (反)11,111,110(转为原码为 10,000,001,十进制为 - 1),计算过程正确。

可见反码的出现完全是为了简化计算机的底层计算而存在的,这种计算方式忽略了符号仅使用加法就解决了正负数的问题。

反码原理

https://blog.csdn.net/quyanyanchenyi/article/details/108961126

1.十进制减法

通常我们用十进制进行减法的时候通常会出现以下两种情况:
不需要借位:如“456-123”
需要借位: 如“253-176”
对于不需要借位:我们可以直接相减即可
对于需要借位的:我们可以通过一系列操作来免去借位(9是最大的,作为被减数不可能存在借位)
如:
253 - 176 = 253 + (999 - 176) + 1 - 1000
=1077 - 1000
=77
// 此处我们 通过引入 9 的补数 来避免借位

**上面我们引出了补数的概念,一个数的补数即为:在这个数所处的进制系统里,这个数的每位最高位 - 这个数 + 1。
1.这里为什么不直接用1000-176呢?因为999-176不需要借位,运算更加方便。

2.为什么补数是1000-176呢?我们使用0-499表示正数0-499,500 -> 999之间的数表示负数-500 -> -1,我们想要将这里的负数定位到钟表中的对应位置,然后便可以直接顺时针旋转指针,相当于做加法,最后再将值进行转化为结果。
这里有几种情况,看下方的钟表图片:**


3.为什么还要-1000呢?我们这里有两种解释:之前加了1000,这里就要减掉1000,使总的值保持不变;也可以这么理解,这里得到的值为结果的补数形式,我们要将其重新化为结果。
会不会有算的结果是正数100,结果-1000误算成-900呢?这也不会,因为需要进行反码运算至少要有一个负数存在,这里只会算出-500(500)-> (2000) 内的值,你看到的100其实是1100,肯定是需要减1000的。而两个正数直接运算即可,不需要如上方法

在这里我们运用了:“被减数 - 减数 = 被减数 + 减数的补数 - 减数每个位最高位
————————————————————————————————————
那么对于结果是负数的该怎么办呢,比如:“176-253”
我们仍用上述的方法来试一下::
176-253 = 176 + (999 - 253) + 1 - 1000
======= = 922 - 999
// 我们可以看到这里还是得借位,不过我们只要把减数和被减数调换位置,然后在前面加上负号就可以了:-(999 - 922)

在被减数小于减数的时候,我们仍然运用了:“被减数 - 减数 = 被减数 + 减数的补数 - 减数每个位最高位,只不过最后将减数和被减数调换了一下位置,然后再加个符号

2.二进制减法

上述例子:”253 - 176“ 的二进制减法:

    11111101
   -10110000
  -------------------
  =11111101 + (11111111 - 10110000) + 1 - (100000000)
  =101001101 - 100000000
  =1001101
  =77(十进制)

不知道聪明的小伙伴发现了没有,在二进制系统中求一个数的补数,其实没有必要使用减法,由于二进制系统只有”0“,”1“构成,求一个数的补数只需要把那个数的0变成1,1变成0即可,所以上述过程可以简化为:

    11111101
   -10110000
  -------------------
  =11111101 + (01001111)+ 1 - (100000000)

1. 这里为什么也用11111111 - 10110000,不用100000000 - 10110000,因为第一种算法就是求反码,直接将各位取反即可,非常的方便运算,但是要是从补数的概念上讲,还是第二种。

2.为何最后的公式是:取反(正数(反码)+负数(反码))?
11111101 + (01001111)+ 1 - (100000000),我们已经确认结果等于这个式子的运算结果了!来就来倒着推一下。
首先正数的反码和原码一致,先不用管;01001111就是负数反码,将二者相加;
这里现在其实得到的值+1就是结果的补数值,然后我们再- 1000000000 求补数得结果,具体情况可以参考上面十进制的情况(二进制里面也是分两部分,0000000-01111111 代表0 -> 127, 而10000000 - 11111111 代表 -127 -> -0)

3.但是我们为了简化运算,并不先运算+1,而是先运算1 - (100000000) = -11111111,我们在上面得到的规律,11111111 - (较小数)= (较小数取反)
那么 (较大数)- 11111111 呢?这里由于使用可溢出系统,结果中大于11111111的位数部分被我们省略,然后我们就会发现,也是将该数后面的位数取反,便可得到结果,这也是取反(正数(反码)+负数(反码))的由来

  =101001101 - 100000000
  =1001101
  =77(十进制)

对于 ”176 - 253“:

    10110000
   -11111101
  -------------------
  =10110000+ (00000010)+ 1 - (100000000)
  =10110010 - 11111111
  = -(11111111 - 10110010)
  =- 77(十进制)

2.3 引入补码(解决0和-0的问题)

反码相较于原码进行了优化,但还存在某些问题,如果上面的 1-2 是 2-2 的话,那么结果就是下面的:

(原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)
(反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0),

终于说到这个 -0 了,这个从人的角度来看当然就是 0 的意思,但是对于计算机来说,还需要编指令告诉它:-0 就是 0,不然在计算机眼里它是两个不同的数字或信息。
这样一来,就又是增加了计算机的复杂度,所以为了方便和效率,人们想出让 -0 表示-1,-1 表示 -2…… 这样的方式,结果就使得负数会比正数多一个数字的情况了
——所以使用这种表达方式的原因并不是为了多增加一个数字范围,而是降低计算机的计算复杂度,毕竟前者相比后者,根本不值一提。

但是使用了这种表示方式后,计算机要怎么计算呢?
这时候就需要用到补码了。(知识点来了,补码的转换过程:正数的补码是其本身,负数的补码是除了符号位外其他位全部取反后加 1,也就是反码再加 1;补码转正码或反码只需要逆转此过程)

第四步:

这时候我们要讲清楚的是 -0 的问题,所以我们拿之前的例子来说明,将原码转为补码后的计算过程为:

2+(-2)
(原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)
(反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0)
(补)00,000,010 + (补)11,111,110 = (补)100,000,000(转为正码前需要舍去高位 1,因为我们本来就是只有 8 位的,现在变成 9 位了,是存不下的,结果为 00,000,000,十进制为 0)

这时候就再也没有 -0 出现了,而舍位操作并没有对计算过程造成任何影响,并且还将 -0 转成了 0(100,000,000 是 - 0,00,000,000 则是 0)

第五步

而且可以用[1000 0000]表示-128:

(-1) + (-127) 
= [1000 0001]原 + [1111 1111]原 
= [1111 1111]补 + [1000 0001]补 
= [1000 0000]补
= -128

补码原理:

1.对于式子11111101 + (11111111 - 10110000) + 1 - (100000000)
我们在反码中是理解为了11111101 + (11111111 - 10110000) - (11111111)
将这个1 - (100000000)提前进行了运算

2.而补码则不一样,11111101 +[ (11111111 - 10110000) + 1] - 11111111 + 1
实际上的运算过程是这样的,而[ (11111111 - 10110000) + 1]相当于在之前的基础上向后推进一个,空出来的那个填-128,也就是原来(10000000->1111111 表示 -127 -> -0),现在(10000000->1111111 表示 -128 -> -1)

附上原码,反码,补码对应真值表

3. 总结

因为计算机最终是保存补码进行计算的,这样表示的负数就总会比正数多一个数字

posted @ 2023-10-18 21:33  DawnTraveler  阅读(116)  评论(0编辑  收藏  举报