一篇就够--有符号整数、反码以及补码溯源 2019-10-09(未经允许禁止转载)

1.计算机是如何存储“数”的

计算机只认识0和1,显然是通过0和1组成的序列来存储数值的,也就是二进制存储

如:8位长度(8bit, 即一个字节, 1 byte)的二进制数 1111 1111表示十进制数255(28-1),0000 1111表示十进制数的15(24-1)

2.计算机的有符号整数和无符号整数

正如上面提到的例子那样,如果二进制数的每一位都是数值位,那么它是无符号整数,也就是非负整数;但显然我们需要计算机能够表示负数,于是引入符号位的概念

还是以单字节长度(8 bit)的二进制数为例。我们(注意是我们,不是计算机)将最高位设定为符号位:0表示正数,1表示负数。如 0000 0001表示+1,1000 0001表示-1,这样,我们借助符号位表示了负数。最高位为符号位的表示方法称为原码表示法,这样的二进制数称为原码

2.1 符号位带来的问题

对于8位长度的无符号整数,可以表示0-255共256个整数;但对于8位长度的有符号整数,按上面的规则,可以表示1111 1111 - 0111 1111即-127到127共255个数。因为在符号位的规则下,1000 0000表示-0,0000 0000表示+0,这两个不同的二进制序列实际上表示了同一个值,因此只能表示255个整数

这样一来,两个不同的二进制原码表示同一个值,

  • 1.一方面增加了计算机的加法复杂度,使计算机的底层逻辑难以设计。因为(+-0)是两个不同的二进制数,但对它们分别进行加法操作却要输出相同的结果,这实现起来是更加复杂的
  • 2.另一方面计算机也浪费掉了一个数

2.2 值重复问题的解决办法

为了解决上面提到的二值为0数值浪费的问题,我们将 0000 0000定为0,而1000 0000我们视为-128。这样一来,8位长度的有符号二进制数表示的范围就是-128到127,共256个数

那么,在这样的基础上,计算机是如何实现有符号整数的加减的呢?

3. 计算机有符号整数的加减法

3.1 要把我们设定好的规则告诉计算机

显然,减法本质上是加法,减去一个数等于加上这个数的相反数。A - B = A + (-B). 因此,我们只需要关注加法。那么,计算机直接拿有符号二进制数相加就可以了吗。如下面拿8位二进制数讨论:

    0000 0001
+   1000 0001
------------------
    1000 0010
可以看到,如果在计算机里直接把有符号二进制数相加的话,并不能得出正确的结果,正像上面的 1+(-1)= -2 是错误的

这是因为计算机本质上只知道傻傻地将二进制序列按照二进制加法法则进行运算,而不知道哪个是符号位哪个是xx位哪个又是什么位。什么?明明上面我还说了将最高位设定为符号位,怎么现在又不知道哪个是符号位了呢?注意,上面说的是我们(注意是我们,不是计算机)将最高位设定为符号位,但是计算机并不知道啊,它不知道的情况下当然将符号位当成正常的数值位进行运算,结果当然是错的。所以,在我们设定好规则的时候,一定要告诉计算机。这里要告诉计算机,8位二进制数的最高位是符号位,不能傻傻直接加了。我不要我觉得,我要计算机觉得

3.2 把我们的规则变成计算机程序的规则

好,你说不能直接加,那配置一下,让计算机自己判断,遇到最高位就忽略。但是,这样就可以了吗?就像我们人一样,假设我们只会非负整数加法,拿到负整数有关的加法运算,难道简单地忽略正负号就可以了吗?看下面的例子

    + 3
+   - 2
------------
    ? 5
可以看到,我们人进行负整数有关的加法运算,如果简单地忽略符号位,只对数值位进行运算,
显然先不管结果的符号正确与否,在绝对值上都是错的。同理,计算机要是也这样做,当然也会造成同样的错误:
    0000 0001
+   1000 0001
------------------
    ?000 0010
结果是?2,实际上是0才对

既然我们人按这样的规则做运算得不出正确的结果,那么计算机也同样不行。规则首先得在我们人这里跑得通,计算机才有可能跑得通

3.3 有符号整数加法规则

计算机中,不管是在内存还是硬盘,符号数一律用补码表示
即原码一律转换为补码后再进行运算,运算时符号位和数字位一起参加运算。同样,运算结果也用补码表示

什么是补码(部分内容参考百度百科):

**补码是在某个模系统中,可以实现 结果无差异运算 的 数集**(这句话是本博文的核心)

“模”是指一个计量系统的计数范围。如时钟,计算机也可以看成一个计量机器,它也有一个计量范围,即都存在一个“模”
例如:时钟的计量范围是0~11,模=12。表示n位的计算机计量范围是0~2(n)-1,模=2n

取“模”实质上是某个值在计量器产生“溢出”的量,即余数。某个值超越了计量器的可表示范围,在计量器上表示不出来,计量器上只能表示出模的余数。任何有模的计量器,均可化[减法]为[加法]运算。
例如:假设当前时针指向10点,而准确时间是7点,调整时间可有以下两种拨法:一种是倒拨3小时,即:(10 - 3) mod 12 = (10 + (-3)) mod 7;另一种是顺拨9小时:(10+9) mod 12 = 7,那么,在模12的系统下, - 3 是等于 + 9的

其实这里面的本质是,(10 - 3) mod 12 = (10 + (-3)) mod 12 = (10 + (12-3)) mod 12 = (10 + 9) mod 12

对“模”12而言,我们说,-3和+9互为补码,因为它们可以实现模内结果无差异的运算。实际上,{..., -15, -3,+9,+21, ...} 这个数集中的数都互为补码。以12模的系统中,+11和-1,+10和-2,+8和-4,+7和-5,+6和-6也都有这个特性

对于计算机,其概念和方法完全一样。n位计算机,设n=8, 所能表示的最大数是1111 1111,若再加1成为1 0000 0000(9位),但因只有8位,最高位1自然丢失。又回了0000 0000,所以8位[二进制系统]的模为2^8。在模系统中,加一个数 = 加这个数对应的补码,如果这个数是负数,那么减法问题通过补码就化成了加法问题

计算机中,借用补码的思想,就可以将有符号数的运算转化为无符号数的运算,因为补码是非负的、无符号的

非负数的补码等于本身,负数的补码等于 对应正数的原码的反码+1

如:原码1000 0001 表示有符号数 -1,对应正数为+1,原码为0000 0001,取反码为1111 1110,再加1等于1111 1111,即无符号数255。**这样就实现了有符号数-1向无符号数255的转换;并且,-1和255正好是模256下的一对补码,因此对-1操作和对255操作是结果无差异的。那么,8位的计算机加上有符号数-1(原码1000 0001),实际上就等于加上其补码(无符号数)1111 1111

一些负数对应的补码
有符号数-1 -> 1111 1111B -> 无符号数255
有符号数-2 -> 1111 1110B -> 无符号数254
...
有符号数-128 -> 1000 0000B -> 无符号数128

可见,uint8转换到int8的对应关系是,
uint8的0-127仍然对应int8的0-127不变,而uint8的255-128对应int8的-1到-128

为什么用补码:

系统对有符号数和无符号数的加减法都通过补码实现,完美适配原来的简单高效的无符号加法法则


例题:leetcode数字转换为十六进制数

给定一个32位有符号整数,编写一个算法将这个数转换为十六进制数。如

输入:26 输出:"1a"
输入:-1 输出:"ffffffff"

思路1:
最朴素的思路就是,有符号十进制数和有符号16进制数的转换
我们需要解决的其实就是负数的转换。我们知道,在32位有符号整数的表示范围内,有符号数 -n 和 无符号数2^32 - n在计算机内部的二进制表示是相同的,实际上就是在mod 2^32 下,-n = -n + 2^32。 这样,我们就可以将有符号数全部转换为等价的【最朴素的无符号整数】,再向其他进制转换轻而易举了
总结:【10进制有符号数->特定模下转等价表示的10进制无符号数->无符号数可随意进行其他进制变换】

class Solution:
    def toHex(self, num: int) -> str:
        d = {10:"a", 11:"b", 12:"c", 13:"d", 14:"e", 15:"f"}
        MaxInt32 = 2**32
        # 将负数转换为相对应的无符号正数,可以直接使用转换后的正数进行进制转换
        if num < 0:
            num = MaxInt32 - (-num) # 如-1就对应无符号32位整数ffffffff
        
        res = ""
        while num != 0:
            m = num % 16
            num = num // 16
            if m not in d:
                res += str(m)
            else:
                res += d[m]
        if res == "":
            return "0"
        return res[::-1]

思路2:
更直接的,数,在计算机内都是反码存储的,而反码本身就是二进制的,有了二进制转16进制,很容易吧

class Solution:
    def toHex(self, num: int) -> str:
        if num == 0:
            return "0"

        hexstr = "0123456789abcdef"
        res = ""
        n = 0
        # 算数由位移,正数补0,负数补-1,因此需要通过计数控制。32位数,一次右移4位,共8次
        while n < 8:
            temp = num & 0xf
            res = hexstr[temp] + res
            num = num >> 4
            n += 1
        for i in range(8):
            if res[i] != "0":
                return res[i:]

本文为作者原创,未经允许禁止转载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。