进制存储和运算(1)——有符数与无符数

来源:互联网 发布:淘宝卖家订单能删除吗 编辑:程序博客网 时间:2024/06/05 06:30

最近补基础知识,把一些内容通过博客记录下来,以便今后参考 

 

 

            我们知道,计算机系统设计的一个目的,就是将存储空间划分为方便管理的单元。关于为什么用二进制和十六进制来处理数据也不用我多废话了,看下例子:   

    比如10进制23=2*10^1+3*10^0,那么16进制0x23=2*16^1+3*16^0=35

    如果是2进制,100011=1*2^5+1*2^1+1*2^0=35=0x23

    我们发现10对应2,而0011对应3,所以0x23很容易通过8421码写出其2进制形式。

 

 关于大端小端:

    这里就遇到个问题,假设有个int a;a有四个字节空间,假设其物理地址分别是

    0x00,

    0x01,

    0x02,

    0x03

    现在要存储0x12345678这个数,很明显,这个数确实需要四个字节的空间才能存下,那么到底是从上往下存,还是从下往上存呢?这里就涉及到大端(big endian)和小端(little endian)概念。

    如果按大端存法,从起始地址0x00开始存最高位字节12,然后地址按权位降低而往下递增;如果按小端存法,则刚好相反:低地址存低权位,高地址存高权位。

 

 

 

小端存法

地址标识

大端存法

78

0x00

12

56

0x01

34

34

0x02

56

12

0x03

78

 

    尽管从直觉上我们觉得貌似右边的存法更符合习惯,然而一般我们接触的系统环境,都是采取小端存法。至于大小端的这个端(endian)的来历,有这么一则趣闻:“来自于Jonathan Swift的《格利佛游记》:Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。Danny Cohen一位网络协议的开创者,第一次使用这两个术语指代字节顺序,后来就被大家广泛接受。

 

    按理说,如果为了更好的管理存储空间,那么某段存储空间所存储的类型应该是确定,这样在系统对数据进行理解时才不容易产生错误,因此大多数应用编程都不推荐对存储对象进行强制类型转换操作。然而,以C语言为例,对系统级编程、嵌入式编程方面,这种操作是必须的。比如,要验证所使用的系统环境是大端还是小端,可以使用如下方式进行验证:

 

      int a = 0x12345678;

      int i = 0;

      for(i=0; i<4; i++){

              printf("%d\n", ((unsigned char *)&a)[i]);

      }

 

    以上代码通过打印就能得出上面表格所展示的顺序,反正我运行出来是倒着的:)

 

    C语言对二进制的布尔运算&、|、~、^,只能运用到整型的数据类型,其应用之一便是网络IP中的掩码概念,通过&运算性质实现。比如任何16进制的数,如果做&FF操作,则必然只保留一个字节的数据,而更高权位的值都全部置0。而我们常见的IP:192.168.1.127,mask:255.255.255.0(FF.FF.FF.0)。当IP&mask时,结果保留IP值中最末一个字节127的值,而前面的192.168.1属于网关端IP。

   

有符数和无符数理解:

    无符数比较好理解,这类二进制数所有位均表示正整数;

    而关于有符数,一般采取补码方式。C语言的标准里并没有规定必须用补码,不过几乎所有机器都认同补码,所以它是事实上的C标准。

    都晓得有符数里的正整数和无符数表达类似,而负整数却多定义了符号位,关于这个符号位很神秘,貌似是专门定义设置的特殊位?那么符号位在运算时到底是什么意义呢?例如:有符数0x1111,它的值是-1,why?

    要搞清楚这点,首先要搞清楚有符数0x1000,他的值是-8,也就是-1*2^(3)……发现没?首位的符号位,其实就是负权的意思,而其余的位仍然是正权:

有符数0x1001=-1*2^(3)+1*2^(0)=-7……

    这里已经很明确了,当4bits的16进制有符数,最小值肯定是0x1000,其中首位是负权(-8),后三位都是正权,正权值越大,做加法时抵销的负数越多,有符数的值自然就越大。再来看0x1111why等于-1?因为首位符号位值是-8,而剩余的111值是+7,

于是-8+7=-1

所以0x1111=-1*2^(3)+1*2^(2)+1*2^(1)+1*2^(0)=-1

 

    另,当考虑无符数时,0x1000就是+8,而0x111还是+7,

    我们发现1*2^(3)-1=1*2^(2)+1*2^(1)+1*2^(0),也就是说0x1000-1=0x0111

 

    有了这个两个基础,我们再想求出有符整型int(4字节32位)的最大值和最小值,是不是感觉特别容易了?

    Tmax=2^(31)-1=2147483647 = 0x7FFFFFFF

    Tmin=-2^(31)=-2147483648 = 0x10000000

 

    我们知道,大多数人在理解补码时,还是习惯利用取反加一的办法来计算正负数转换,而并非理解了补码、理解符号位的实质意义。再退一步讲,按位取反加一的实质原理,估计也不是每个使用者都清楚,那么接下来我就试着分析一下。

    当我们需要改变有符数的符号时,比如-5变成5,最简单的方法就是-x=~x+1和-x=~(x-1),前者放之四海而皆准,而后者x!=0。两个公式成立的共同依据(我觉得),应该是对于有符数x,x+~x=-1,而相加得-1的原理(还是我觉得),乃是~操作本质就是按位颠倒,那么x和~x的所有位都不相同(某个bit上你是1我就是0,你是0我就是1),于是乎,x+~x的结果肯定为全1(两者相加每个bit上有且仅有一个1),当然就是-1了!

    这就是补码最神奇的一个现象。更神奇的是,我们还可以像原码那样,做补码的二进制加减法,例如:-7+3

100100111100

 

上图像是做无符数加减法样得出1100,其补码值刚好等于-4 :-)

 

                   还有个奇特的数字x=2^(w-1),有x=~x+1,这个数字是绝对的例外。

    回到上面Tmax和Tmin,遇到个问题,如果要用宏定义表示Tmin时可能出现问题:

#define Tmin -2147483648

    当编译器在处理-X这一类型的数据时,是先读X,再读-,当处理2147483648时,明显超过了有符正整数的最大值Tmax。

我能想到的有三种替代办法可以避免此类问题

#define Tmin -2147483647-1

#define Tmin -(2147483648U)

#define Tmin 1<<31

     现在的编译器经过优化,容许并能够正确处理-2147483648,编译时会产生waring,不过我们在开发时应该还是尽可能的避免。

 

 

有符数和无符数转换

    C语言里经常会遇上有符数和无符数之间的转换问题,为这里就来讨论下转换的公式。为了描述方便,用X表示一段二进制数,w表示X的二进制位数,用T表示有符数,U表示无符数。

 

    对于w位的有符数X,转无符数:T2U(X)=Xw-1*2^(w)+X

    此公式很好理解。首先,如果有符数X为正数,那么符号位Xw-1肯定为0,

此时T2U(X)=X毫无悬念;

 

    如果有符数X为负数,那么符号位Xw-1肯定为1,该位在有符数的环境下为负权,即-1*2^(w-1)。现在要把X理解成无符数,那么符号位Xw-1就应该算成是1*2^(w-1),于是,T(X)和U(X)应该相差两个1*2^(w-1),

U(X)-T(X)=1*2^(w-1)-(-1*2^(w-1))=1*2^(w)……正权部分相同所以抵消掉

U(X)=1*2^(w)+T(X)===>T2U(X)=Xw-1*2^(w)+X

 

    例如X=0x1010,此时w=4,T(X)=-6,X4-1=1,X2-1=1

U(X)=1*2^4+(-6)=10

 

    同理,对于w位的无符数X,转有符数:U2T(X)=-Xw-1*2^(w)+X

    当无符数X的最高位Xw-1为1,转换成有符数时会产生负权,相差两个-2^(w-1),于是要补-1*2^(w)。于是T(X)=-1*2^(w)+U(X)。

 

 

求模(余数)运算

    求模运算(mod)对非数学专业来说相对陌生,而求余则是小学数学内容,我们总觉得自己在这个内容上没啥想不通的,果真如此么?来看几个例子:

9mod8,9除以8余几?答案是1,妥妥的没错,那么再作下一道题:

-9mod8?(╯﹏╰)b,感觉还好吧?其实也简单,有两个答案,下面是演算过程:

 

-9mod8 = -9+1*8 = -1……商1余-1

-9mod8 = -9+2*8 = 7 ……商2余7

 

-9mod8的余数到底是-1还是7呢?小学数学的定义是以最小的数作为余数,可小学没学过负数不是?因此-1和7都是-9÷8的余数。

从十进制的规律来看,如果除数是10^n,那么112mod100=12,112mod10=2,可以看出,除数是截断大于或等于自己的全部权位,而保留比自己小的权位作为余数结果的,那么二进制也是类似。

-9:0x10111

8 : 0x1000

    按照十进制的规律,-9mod8 = -9mod2^3=111,问题就来了,前面补0还是补1呢?最终余数结果到底是0x0111还是0x1111?

    关键就看你把结果111当成有符数看还是无符数看,当成无符数,自然满足逻辑右移,取值为0x0111(7);而当成有符数的话,自然满足算数右移,于是取回位0x1111(-1)。

    不同的机器可能有不同的选择,一般来说,mod运算会把截断数字视为有符号数,因此得-1的机器占多数。

 

有符数和无符数加法和乘法 

    上面扯求模运算可不是为了掉书袋,而是有真实用途的,接下来先讨论无符数的加法。如果对于两个w位的无符数Xu,Yu<2^w,做加法结果完全有可能溢出:

(Xu+Yu<2^(w+1)),使得w位无符数不能表达,这时就涉及到截断(X+Y为截断后实际值,Xu+Yu为相加的逻辑理论值,下同)。 

 

                   Xu+Yu     (Xu+Yu<2^w

X+Y=

                  Xu+Yu-2^w  (2^w<=Xu+Yu<2^(w+1))

 

    从上面的公式可以看出,从Xu+Yu>=2^w开始往上增加时,结果会减去一个2^w,根据上面的求模运算规律,相当于做了一个Xu+Yu mod 2^w,其实质就是把加法溢出的进位给减掉了。

 

    无符数的加法很好理解,比较麻烦的是有符数的补码加法。对于两个有符数Xt、Yt,有-2^(w-1)<Xt,Yt<2^(w-1)。那么两数的和-2^w<Xt+Yt<2^w-2,也有部分是溢出的

 

       Xt+Yt-2^w  (2^(w-1)<Xt+Yt)

X+Y =   Xt+Yt      (-2^(w-1)<Xt+Yt<2^(w-1))

        Xt+Yt+2^w   (Xt+Yt<-2^(w-1))

 

    从上面的公式可以看出,当Xt+Yt的和正溢出时,由于有多溢出的进位Xw,因此应该减掉2^w,当和在有符数范围内自然是等于本身,而当和负溢出时,由于有溢出的进位并且该进位是负权位(符号位)Xw,因此应该加上个2^w将其抵消掉……

 

    无论正溢出还负溢出,其处理方式的实质都相当于做mod 2^w运算。还有我们看到一个规律那就是,w位的数相加,最多溢出到w+1位,而不可能溢出更多的位,这个用十进制来理解比较容易:两位数相加,最大值莫过于99+99=198,变成三位数,不可能加成四位数,于是无论用+-2^w还是用mod2^w都是在运算上都是完备的截断。

   

    乘法是另一种意义上的加法,但是乘法可能要溢出到2^(2w)位,不过没关系,还是用mod2^w搞定,因为不管你是正数还是负数,不管你怎么进位,你的w位一定是确定的,事实上补码的乘法运算也是和原码完全一样。

0 0