位运算 补码 反码

很多初学者经常搞不懂补码反码原码这些东西,今天我就在这里好好理一下。

基本运算

先来讲讲基本的与或等运算,会的可以直接跳过

与 AND 或 OR 异或 XOR 非 NOT
1,1 1 1 0
1,0 0 1 1
0,0 0 0 0
1 0
0 1
有0则为0 有1则为1 异常的或,在都为1的时候为0 取反

容易看出,与,或,异或是二元运算(即有两个操作数,如加减乘除也是二元运算),且满足交换律。而非是一元运算。当然也可以有多位一起参加运算,如10 ^ 11 = 01,各位运算是独立的

另外,与,或,异或也满足结合律:

1
2
3
(a & b) & c == a & (b & c)
(a | b) | c == a | (a | c)
(a ^ b) ^ c == a ^ (b ^ c)

其中& ,|,^ 在C语言中分别表示与,或,异或运算,现在来证明一下:

与:只要位上有1,结果必是1
或:只要位上有0,结果必是0
异或:位上1的个数为奇数,结果就是1,反之为0


原码,反码

现在来讲讲正题。首先我们要知道补码和反码是两种储存数据的方式

大家都知道计算机里存储的是二进制数,如果我们用八个位表示一个整数,
2 = (00000010),其中00000010就叫做2的原码

这样我们就可以用八位表示0~255的任意整数了。但是我们如何表示负数呢。我们就想出了反码这种东西。要理解这个还得了解一下计算机如何执行加法。

1
2
3
2+1 = (00000010) + (00000001) = (00000011) = 3
2+2 = (00000010) + (00000010) = (00000100) = 4
2+3 = (00000010) + (00000011) = (00000101) = 5

可以看出就是两个加数各位异或,再加个与运算判断是否进位。(可以用这个原理在MC用红石搭个简单的加法器)。

所以我们如果要表示负数(最关键的问题),可以用 a + (-a) = 0来定义表示方法。我们不妨定义(11111111)为-0(也就是0)这样我们我们就可以把一个数各位取反来表示她的相反数。如(~表示取反)

1
2
3
4
5
6
7
8
9
10
11
2 = (0000 0010)
~2 = (1111 1101) = -2 //这些操作在实际机子上不成立,因为我们现在是以补码方式存储数据
2 + -2 = (1111 1111) = 0
---
0 = (0000 0000)
~0 = (1111 1111) = -2
0 + -0 = (1111 1111) = 0
---
127 = (0111 1111)
~127 = (1000 0000) = -127
127 + -127 = 0

这就是反码,指的不仅是按位取反,还指这种存储数据的方式

由此可见,(1000 0000)不能再像原来那样表示128了,因为他现在表示-127,以及128以上数(最高位为1)的表示方法同样被抢走了,所以我们就不表示这些数,那现在我们用八位表示的整数范围便变成了[-127,127]

我们还可以知道,最高位是0的数现在都是正数,而正数取反后得到负数,所以负数的最高位是1(不严谨的讨论,-0看作负数,0看作正数^_^)所以我们现在把最高位叫做符号位

补码

可是我们知道我们现在都是以补码方式存储数据,补码又是什么,相对于反码又有什么优势呢

补码也叫做二补数,而刚刚介绍的反码叫做一补数,这也可以看出补码是比反码先进一点。

仔细看看我们刚刚的反码,我们用了(0000 0000)和(1111 1111)两种方式表示0,这使0的编码不唯一了,很多情况下我们需要特殊判断,导致效率低下(特别是电路设计中)。所以我们用补码解决了这个问题。

补码和反码的区别就在于负数的表示。回到最初的问题,我们需要表示负数,但现在我们不能再用(1111 1111)表示0了,要坚持一个0原则(0000 0000),可我们的 a + (-a) = 0还是要成立的,所以

1
2
3
2 = (0000 0010)
2 + -2 = 0 = (0000 0000)
-2 = (1111 1110)

我们就有了另一种数据存储方式,(1111 1110)就是-2的补码。现在我们来研究一下她的性质

我们容易得到:(1溢出到最高位之外了,我们不要了)

1
2
(1111 1111) + (0000 0001) = (0000 0000)
(0000 0001) = 1

可得(1111 1111)表示-1

还有:(再说一次~表示取反)

1
2
3
4
5
6
7
2 = (0000 0010)
~2 = (1111 1101)
2 + ~2 = (1111 1111) = -1
2 + ~2 + 1 = 0
---
a + ~a = (1111 1111) = -1
a + ~a + 1 = 0

所以我们得到(~a+1)就是a的补码,也就是我们常说的补码等于反码加一

这样子我们就解决了0的唯一性,现在我们有:(可以去和上面的反码方式对比一下)

1
2
3
4
5
0 = (0000 0000)
-0 = ~0 + 1 = (1111 1111) + 1 = (0000 0000)
---
127 = (0111 1111)
-127 = ~127 + 1 = (1000 0000) + 1 = (1000 0001)

那我们现在就可以比反码多表示一位了,可以看出(1000 0000)这一种表示方法没有被抢走,所以我们就可以用(1000 0000)表示-128。所以我们能表示的整数范围现在就是[-128,127]。

这就是补码和反码两种存储数据方式。


我们可以用程序实验一下计算机里是如何用补码存储数据的:

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
for (int a = 0;a <= 10;++a)
printf("%d %d %d\n",a,~a,~a+1);
return 0;
}

程序输出为

1
2
3
4
5
6
7
8
9
10
11
0 -1 0
1 -2 -1
2 -3 -2
3 -4 -3
4 -5 -4
5 -6 -5
6 -7 -6
7 -8 -7
8 -9 -8
9 -10 -9
10 -11 -10