uchar a = 0 ;
while (1 )
{
a++;
}
执行上面的代码,记录a的取值将会得到这样的结果: 0,1,2,……,255 ,0 ,1,2,……
我们发现255之后紧跟的是0。从二进制来看就是说[1111 1111]加1之后的结果是[0000 0000]。学习过数字电路加法器的同学并不会觉得有什么不妥。代码中的这一现象就是数字电路的特点决定的。
我们生活中也有类似这样的例子。时针的取值在1到12之间循环,也可以认为是在0到11之间循环,更符合编程和数学习惯。
上面两种情况都可以用一个数学工具来刻画——模 。简单来说,给定一个正整数N作为模,对于任意一个整数n对N取模,就是加上或者减去若干个N,使其落在0到N的范围内。举两个例子,我们给定N=13,56对13取模就是56 - 4 * 13 = 3,-17对13取模就是(-17) + 2 * 13 = 9。不管n取什么值,对13取模的结果只能是0到12之间。
接下来研究整数在内存中是如何表示的,先来看几个基本数据类型:
数据类型
字节数
范围
uchar
1
0~255
char
1
-128~127
short
2
-32768~32767
每一种数据类型都是由固定长度的二进制组成,表示一定范围的整数。uchar和char都是一个字节长度,表示256个整数,不同的是uchar表示的都是正数,char表示的有正有负。
uchar a = 3 ;
uchar b = 254 ;
uchar c = a + b;
char a = 3 ;
char b = -2 ;
char c = a + b;
上面分别是两个类型的加法运算,如果输出结果你会发现两个示例c的结果都是1。结合下面一张表来分析
uchar
二进制编码
char
3
0000 0011
3
2
0000 0010
2
1
0000 0001
1
0
0000 0000
0
255
1111 1111
-1
254
1111 1110
-2
253
1111 1101
-3
252
1111 1100
-4
观察可以发现,两个示例中的变量a,b,c对应的二进制编码完全一致。也就是说两个示例在计算机内部本质上并无二致。
仔细想想,其实也好理解。由于计算机遵循着模的运算,-2对256取模的结果是254,而3加254再对256取模的结果是1。uchar和char只是人为做出的规定,一个全部表示正数,一个既有正数也有负数,就像物理中选择了不同的参考系,看问题的角度不同而已。所以uchar和char的类型强制转换也只是切换参考系而已。
现在我们正儿八经开始讲原码,反码和补码。我们以-2为例
原码
反码
补码
1000 0010
1111 1101
1111 1110
可以看的出来-2的补码和我们上面表中的编码是一致的。书上也说了,计算机中就是补码的形式。
Q : 既然这样,干吗还弄个原码和补码出来?
A : 看看原码和补码,哪个更容易联想到-2?
显然忽略最高位,原码就是2,最高位1表示负数的话,整个原码就是-2了。而补码则有点费劲。原码,反码到补码其实提供了一种计算机器码 的方法。
1. 最高位始终用1表示负数,低七位表示绝对值,就形成了原码。
2. 原码 ===高位保持不变,其他位取反===> 反码 。
3. 反码===加1===>补码。
所以,反码完全沦为一个过渡。这里-128无法使用这一方法得到补码,因为我们无法只用7位为128编码。
Q : 为什么这么算就能得到机器码?巧合?
A : 我们先放一放这个问题,等下再讲。先讲另外一个计算机器码 的方法。
我们再来看254和-2,前面已经讲过-2对256取模的结果就是254,在uchar下254的编码就是char下-2的编码。所以-128到-1的负数加上256得到正数(就是对256取模)在uchar下的编码就是所要求得char类型的机器码。是不是这种方法更本质更自然更简洁一点呢?而且还不遗漏-128。
回过头来看上面的问题,原码到反码再到补码,其实也可以从数学上得到解释。1000 0010低七位取反加1相当于(1111 1111 - 0000 0010) + 0000 0001= 1111 1110即255-2+1=-2+256=254。(注意:取反可以演变为加法,0101 1010取反=1111 1111 - 0101 1010 = 1010 0101)
以上观点是阅读他人成果加上自己思考的产物,难免有所偏颇,欢迎各位批评指正。