计算机是用n位0和1来表示数字的,这样很容易表示正数,但是怎么表示负数呢?
人类聪明的大脑想到了用第一位来表示符号,0代表正数,1代表负数。这种表示方法最好理解,叫做原码。
但是计算机在计算的时候,为了简化,需要把减法当做加法运算。这个很简单,负数不就是干这个的吗?比如2-1=2+(-1)。
但是负数如果按照原码表示的话,就不好办了,比如:
2-1=2+(-1)=00000010+10000001=10000011=-3
显然是错的,所以人类习惯的方法无法满足这个需求,根本原因在于符号位的引入不符合计算机运行原理。
那么,肿么办呢?
首先介绍模的概念:数学上7位数可以表示[0, 127]共128个值,超过128就会溢出,那么128就是7位数的模。
而模一定的情况下,减法都可以变换成加法来运算,比如:
你要把钟表往前拨2个小时,你可以把它往前拨10个小时来实现。这里-2就转化为了+10。
下面看同余数的数学定义:
如果a和b对于一个数m的余数相同,成a和b为m的同余数,或者称a和b互为补数,记做:
a ≡ b (mod m)
那么问题来了,负数的余数如何定义?
数学上是
就是:x对y的余数等于x减去y乘上x与y的商的下界。
而显然,一个数的余数一定是它的同余数。
这样如果你想算a-b的话(a,b都是正数),可以算成a+(-b的补数),比如:
模为128的情况下,2-1=2+(-1)=2+(128-1)=128+1,由于溢出,结果是1
终于算对了!撒花庆祝!但是还有下一个问题:
给定负数的原码,如何让计算机方便的算出负数的补数?
假如用n位来表示数,那么-a=2^(n-1)-a-2^(n-1),其中a为正数
而a表示成10进制的话,
是a=k0*2^0+k1*2^1+k2*2^2+...k(n-2)*2^(n-2),为什么是n-2呢?因为原码要留一位给符号位,所以只有n-1位有效位。
而2^(n-1)=1+1*2^0+1*2^1+1*2^2+...+1*2^(n-2),所以
2^(n-1)-a=1+(1-k0)*2^0+(1-k1)*2^1+(1-k2)*2^2+...(1-k(n-2))*2^(n-2),也就是除了最高位之外,其他部分取反加1。
而剩下的-2^(n-1),它的补数是2^(n-1),也就是还要再加上个最高位为1其他全为0的数.
那么就得出我们教科书的结论:
对于负数,原码到补码就是:符号位为1,其他位取反加1(不可进位到符号位)。
显然,对于正数,补码等于原码(想想钟表的例子)。
注意一个特例,-128是没有原码的,因为原码的表示范围就是[-127~127],0有10000000和00000000两个。
但是按补码算的话,-127-1=11111111+10000001=10000000
所以补码是可以表示-128的,因此补码的表示范围是[-128~127]
注意,补码的计算完全是2进制的计算,没有符号位,而且该溢出溢出。
总结:
1. 计算机都是用补码表示数,语言在编译的时候已经把原码转换成了补码,再写入内存。
比如
int i=-1;
printf("%x\n",i);
输出就是0xFFFFFFFF
2. 计算机不知道减法,也不知道符号位,它只知道加法和溢出。所以补码运算只有加法和溢出。
3. print的时候,%d是打印原码,%x是打印计算机里的原始数据。而用0x80000000来赋值的话,其实是直接赋值给计算机内存原始数据。
比如
int i=0x80000000;
printf("%d\n",i);
输出就是-2147483648