进制存储和运算(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位一定是确定的,事实上补码的乘法运算也是和原码完全一样。
- 进制存储和运算(1)——有符数与无符数
- 进制存储和运算(2)——浮点数底层探秘
- 进制的存储和运算(3)——浮点数强制转换小议
- &运算(与运算)和|运算(或运算)
- 进制与运算
- 大整数存储和运算
- 进制运算与位运算
- 黑马程序员——C语言——位运算符和变量存储原理
- 位结构(按位运算和存储)(转载)
- JS 中的||(或运算)和&&(与运算)
- 运算和运算符——Python
- opencv——融合技术(alpha融合 和 cvAnd按位与运算)
- 矩阵分析与应用(一)——集合的基本运算和内积空间
- 巧用减1和位与运算
- 46 进制与位运算(上)47 进制与位运算(下)
- c语言---数据的存储与运算
- 海量图片存储与运算架构
- 串的顺序存储结构与运算
- 阿里巴巴面试题汇总
- 关于把图片内容绘制到view中
- struct(在C与C++中的区别)
- 黑马程序员 —— 面向对象(第六天)
- Unit6--problem4--多文件组织多个类的程序
- 进制存储和运算(1)——有符数与无符数
- 第六周项目3用多文件组织多个类的程序
- SGU 194 无源无汇上下界网络流
- c++类的构造函数详解
- Spring transaction 事务 --Isolation & Progation
- syslogd架构
- Eclipse更改默认工作目录的方法
- Android开发之InstanceState详解
- C++的类和C里面的struct有什么区别