zoukankan      html  css  js  c++  java
  • 理解整数为什么存成补码的正确姿势!

    背景问题:你知道计算机中以什么形式存储整数吗?是符号位加值位吗?值位是按照正常的二进制方式存储的吗?假如用3位二进制进行存储,符号位0正1负,1是存成001,-1是存成101吗?
    答:使用补码的方式而不是正常的方式存储,虽然是符号位加值位,但符号位承载的信息和值位的值不是你想象中的方式,比如用3位二进制进行存储,符号位0正1负,1会存成001,-1会存成111 

    首先来回忆一下刚学计算机时候的教材里是怎么解释补码的:

    - 原码表示法是机器数的一种简单的表示法。其符号位用0表示正号,用1表示负号,数值一般用二进制形式表示。
    - 机器数的反码可由原码得到。如果机器数是正数,则该机器数的反码与原码一样;如果机器数是负数,则该机器数的反码是对它的原码(符号位除外)各位取反而得到的。
    - 机器数的补码可由原码得到。如果机器数是正数,则该机器数的补码与原码一样;如果机器数是负数,则该机器数的补码是它的反码在未位加1而得到的。
    - 现代计算机中普遍使用补码表示法,而不是原码。

     WTF!WHY?!

    OK,下面我们来一一解答

    一、计算机为什么使用补码的形式存储整数

      出于简化计算机基本电路的考虑,让加减法都只需要用加法电路实现。所以需要把减去一个正数或加上一个负数都用加上一个正数的方式来表示,于是在存储的时候,负数被直接存储成一种可以直接当成正数来相加的形式 (正数不变,所以以后的讨论中有时候略去正数),这种形式就是补码

    二、那么补码具体是什么?补码是怎么做到加一个数跟减另一个数一样效果的?

    1. 先从时钟这个身边的例子理解 “加一个数跟减另一个数一样效果”
      假设你对钟的时候如果发现它是6点,但实际上现在是2点,也就是它走快了4个小时,你可以有两种做法进行校正,一种是逆时针拨回4个小时到2点,另一种是顺时针拨6个小时到12点然后再拨2小时,也就是顺时针拨8个小时。也就是对于时钟的表盘来说,-4 = +8,同样还会有 -1 = +11,-5 = +7,甚至还有 -4 = +8 = +20 = +32 = -16
      他们间隐藏了什么规律呢?在数学中,-4、+8、+20、+32、-16可以归为符合某个条件的同一类数字——对于模12同余。wiki上对于模的定义是 “两个整数a、b,若它们除以正整数m所得的余数相等,则称a、b对于模m同余”
      而在一个可溢出计数系统中,把计数系统容量作为模,那么所有对此模同余的数在此计数系统中都会有同样的表示(加这个数也一样)。比如时钟表盘就是一个可溢出计数系统,模为12;一个n位二进制构成的计数系统中,因为会舍弃溢出的高位,所以也是一个可溢出的计数系统,模为2^n(从0数到2^n-1)
      所以假设一个3位二进制构成的模为8的计数系统,-2,-10,6,14都表示同样的数,也就是减10和加14是一样的效果

    2. 引出“补码”
      为了让“补码”实现 “加一个正数跟加一个负数(减一个正数) 一样的效果”,“补码”就可以是跟原负数对于模同余的正数
      在计算机中为了减少不必要的运算,负数的“补码”就取其中最小的正数,正数直接就是它自己(而且如果负得太多,补一个模都不是正数,那其实算是左边越界溢出)
      可能是通过原码求“补码”就是一个补模运算,所以把它叫做“补码”
      (但要注意,这里的“补码”都被我打上了双引号,因为这还不是计算机里真正的存储的补码形式,它应该叫补数,不过相信我,已经差不多了)

    三、但这种“补码”表示还有问题

      通过转换成“补码”,减一个数确实变成加一个数了,看似很不错,但却有一个明显的问题,那就是数本身的符号丢失了
      我们只存储了一个在加法运算中方便运算的负数(甚至在结果为负数时计算结果都会不正确),但它却不是一个能正常表示自己的负数(无法逆运算回去)
      比如3位二进制,正常能表示0~7,使用补码法能进一步表示 -8~-1的运算,但不能真正表示 -8~-1

    四、怎么完美解决“补码”的正负表示问题?

      不知道大牛是怎么想到的,但最后我们看到的这种做法,是真的Magic。只需要在左边加一个二进制位来表示正负,就能同时实现这几个效果:

    1. 在保持补码特性的前提下。也就是减一个数还是照样变成加一个数
    2. 增加正负的表示。能真正表示 -8~-1了,就只用看符号位是0还是1
    3. 还能让运算时不用另外区分符号位,直接把符号位当成值位进行运算,而结果的正负号自然会符合这个正负表示法(也就是符号位的进位和值位的进位都会自然地合理)(有一种理解方式是,把这个负号1当成减一个模)

      具体来说是在左边加一个符号位,这个符号位不参与“补码”的运算,始终表示数的正负,但在加法运算中跟值位一样参与运算。加了一个这样的符号位的“补码”就是真正计算机中存储的补码了

    五、最后总结正常人怎么求补码

      对负数求最小正同余数(模为二进制值位存储容量),把它们放入值位,符号位置1

      到这里“负整数为什么存成补码?”这个问题基本就解答清楚了,你会发现里边都没有反码的影子,对,就是这样,用反码以及那套教材里的计算补码的方法来理解负整数的表示都是缘木求鱼,原理上根本不需要它们,那它们是用来干什么的呢?值位取反加一这种算法是怎么冒出来的?接着往下看补充内容你就知道了

    补充计算机中求补码的简便算法

      对于计算机来说,上面这个过程有点繁琐,尤其是需要先求模,然后求最小正同余数,而且这个过程是非常基础的过程,一点点优化都能有很大的性能提升
      然后这个过程就被大牛优化成这样:先直接把负数的绝对值存到值位,符号位为负,然后对值位取反+1,就得到了补码(这也是很多教材里告诉我们补码怎么求的方法。。。用这个理解补码,真是够了)
      能看到其实优化的是求“补码”:值位取反加一 = 最小正同余数,下面可以证明一下:

    1. 用3位二进制值位[abc]表示一个不会造成溢出的负数F:F = -( a*2^2 + b*2^1 + c)(a,b,c ∈ {0,1})
    2. 对F的值位取反 :F(反) = (1-a)*2^2 + (1-b)*2^1 + 1-c = 2^2 + 2^1 +1 - ( a*2^2 + b*2^1 + c ) = 2^3 -1 + F
    3. 然后3位二进制的模等于2^3,所以F的补码 F(补) = F + 2^3
    4. 所以结果就出来了: F(反) = 2^3 -1 + F = F(补) -1 得到 F(补) = F(反)+1

    可能是目前为止最好的理解补码的文章XD,还请博友批评指正~

  • 相关阅读:
    Ubuntu 16.04实现SSH无密码登录/免密登录/自动登录(ssh-keygen/ssh-copy-id)
    简单理解Linux的Loopback接口
    iptables为什么需要增加loopback回环的规则
    [ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配
    [ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
    [ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
    [ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
    [ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
    [ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务
    AOP框架Dora.Interception 3.0 [5]: 基于策略的拦截器注册方式
  • 原文地址:https://www.cnblogs.com/bellkosmos/p/7150105.html
Copyright © 2011-2022 走看看