前言
因为一道题目让我不断地深追下去,挖出了我多年的噩梦——数据类型的范围与长度。每次都想得头痛,因为平台不同、编译器不同、编程语言不同等等因素,又没去做实验,网上那么多说法该相信谁都不知道……那不如趁现在就来详细地解决掉它吧。
一、原码、反码和补码
基础知识
相信在大学的《数字逻辑》课上都学过这个内容了,原码、反码和补码都是基于二进制而言的:
【原码】第1位表示符号位,其余位是这个数的绝对值。这是最简单能够马上想到的表示方式了。
【反码】正数的反码是其本身;负数的反码:在原码的基础上,符号位不变,其余位取反。
【补码】正数的补码是其本身;负数的补码:在原码的基础上,符号位不变,其余位取反,最后+1。
举个例子,假设整数在机器上是用8位二进制数表示的(8位就和我们经常说的32位、64位是一样的含义):
为什么要用原码、反码和补码呢?
原码的来源
为了让二进制能够表示负数,产生了原码。
反码的来源
一个正数和一个负数运算需要辨别符号位,然而单独去辨别符号位会给电路设计带来极大的复杂度,因此人们想只设计加法电路,让符号位直接参与加法运算达到减法的目的,产生了反码。例如:3-2 = 3+(-2) = [0000 0011]反+[1111 1101]反 = [0000 0001]反 = [0000 0001]原=1(注意反码的加法当最高位进位的时候,最低位需要+1,不再详细描述,参考百度百科《二进制反码求和》)。这样符号位就能够参与运算了。
补码的来源
反码看起来很完美,但是仍然存在问题。例如3-3 = 3+(-3) = [0000 0011]反+[1111 1100]反 = [1111 1111]反 = [1000 0000]原=-0,而[0000 0000]反=[0000 0000]原 = +0,也就是说,零可以表示为两种形式,这种歧义同样不利于电路实现。并且由于反码的加减法还需要对溢出位进行处理,于是产生了补码。补码对溢出位直接丢弃,而0的表示只有一种[0000 0000]补,[1000 0000]补则看成是-128,解决了所有问题。
原码、反码和补码的范围问题
值得注意的是,8位的原码和反码都只能表示[-127, +127]范围内的整数,而补码可以表示[-128, +127]范围,多一个-128。这里的-128是计算得到的,而不是从反码推出的,-128根本无法用反码表示,却能够用补码计算,比如-127+(-1) = [1000 0001]补+[1111 1111]补 = [1000 0000]补。所以我们经常背的整数取值范围[-32768, +32767]之类的东西为什么负数总比整数的真值大1,就是这样来的。
计算机中按位取反会发生什么?
既然计算机表示的时候用的是补码,那么如果对十进制的整数【按位取反】操作到底操作的是补码还是二进制呢?
实验一下吧:printf("%d\n", ~(3));printf("%d\n", ~(-3));
【平台】windows 8 64位
【IDE】vs2013 32位
【语言】C语言
【取反操作】~
【取反结果】~3 = -4,~(-3) = 2
数值比较小,最高位没有影响,就按照8位来仔细观察第一组数据:
3 = [0000 0011]b = [0000 0011]