概念

计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。

原码 直接将二进制按照正负数的形式翻译成二进制就可以。
反码 反码是原码除符号位,按位取反。
补码 补码等于反码加一。

正数的原、反、补码都相同。

深入理解

我们都知道,任何存储于计算机中的数据本质都是以二进制码的形式存储,而根据冯·诺依曼的计算机体系架构,计算机由运算器、控制器、存储器和输入输出设备组成。而其中运算器中只有加法器,所以计算机无法直接进行减法运算,只能通过加法来间接实现。就如同数学中的减去一个数,可以看成加上这个数的相反数,当然这样做必须有一个前提,那就是要有符号的概念,于是就引入了符号位

原码,反码,补码的产生过程,就是为了解决,计算机做减法和引入符号位(正号和负号)的问题。

(一)原码

最简单的机器数表示法。最高位为符号位,'1’表示负号,'0’表示正号。

以带符号位的四位二进制为例:

- 正数 - 负数
0 0000 -0 1000
1 0001 -1 1001
2 0010 -2 1010
3 0011 -3 1011
4 0100 -4 1100
5 0101 -5 1101

可以发现出现了’0’和’-0’两个0,但是无伤大雅,先忽略。
接下来开始运算:

0001 (1) + 0010 (2) = 0011 (3) --------------------------------正确
0000 (0) + 1000 (-0) = 1000 (-0) ------------------------------正确
0001 (1) + 1001 (-1) = 1010 (-2) ------------------------------错误

可见正数之间的加法通常是不会出错的,而正数与负数或负数与负数相加就会得出奇怪的结果,这都是由于符号位引起的(包括‘0’和‘-0’的问题)。
于是,人们便发明了反码。

(二)反码

正数的反码还是等于原码
负数的反码就是他的原码除符号位外,按位取反。

原码的问题在于一个数加上它的相反数不等于零,于是反码的设计就解决了这一点,干脆将负数除符号位全部取反。

以带符号位的四位二进制为例:

- 正数 (原码和反码相同) - 负数(原码) 负数(反码)
0 0000 -0 1000 1111
1 0001 -1 1001 1110
2 0010 -2 1010 1101
3 0011 -3 1011 1100
4 0100 -4 1100 1011
5 0101 -5 1101 1010

对照上表再次进行运算:

0001 (1) + 0010 (2) = 0011 (3) --------------------------------正确
0000 (0) + 1111 (-0) = 1111 (-0) -------------------------------正确
0001 (1) + 1110 (-1) = 1111 (-0) -------------------------------正确
看起来好像没问题了,再试一下两个负数相加:
1110 (-1) + 1101 (-2) = 1011 (-4) ------------------------------错误
可以看出计算出问题了,是偶然吗?再试一个:
1110 (-1) + 1100 (-3) = 1010 (-5) ------------------------------错误

由此可见虽然解决了一正一负的两个数相加的问题,却还有两个负数相加的问题。
但是实际上,两个负数相加出错其实问题不大。我们的目的是解决做减法的问题,把减法当成加法来算

两个正数相加和两个负数相加,其实都是加法问题,只是有无符号位罢了。而正数+负数才是真正的减法问题。

也就是说只要正数+负数不会出错,那么就没问题了。负数加负数出错没关系的,负数的本质就是正数加上一个符号位而已。

(三)补码

正数的补码等于他的原码
负数的补码等于反码+1。
( 这只是一种算补码的方式,多数书对于补码就是这句话 )

其实上面那几句话,都只是补码的求法,而不是补码的定义。很多人以为求补码就要先求反码,其实并不是
在《计算机组成原理》中,补码的另一种算法是:

负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。

这句话告诉我们那句‘反码+1’并不是必须的。

补码再深入

将钟表想象成是一个1位的12进制数. 如果当前时间是6点, 我希望将时间设置成4点, 需要怎么做呢?我们可以:

  1. 往回拨2个小时: 6 - 2 = 4

  2. 往前拨10个小时: (6 + 10) mod 12 = 4

  3. 往前拨10+12=22个小时: (6+22) mod 12 =4

2、3方法中的mod是指取模操作,16 mod 12 = 4 即用16除以12后的余数是4。所以钟表往回拨(减法)的结果可以用往前拨(加法)替代。

首先介绍一个数学中相关的概念: 同余

同余

两个整数a,b,若它们除以整数m所得的余数相等,则称a,b对于模m同余,记作 a ≡ b (mod m),读作 a 与 b 关于模 m 同余。

举例说明:

4 mod 12 = 4

16 mod 12 = 4

28 mod 12 = 4

所以4,16,28关于模 12 同余。

负数取模

正数进行mod运算是很简单的, 但是负数呢?
下面是关于mod运算的数学定义:

x mod y = x - y⌊ x / y ⌋, for y ≠ 0
符号⌊ ⌋为 取下界 符号

上面公式的意思是:
x mod y等于 x 减去 y 乘上 x与y的商的下界.

以 -3 mod 2 举例:
-3 mod 2

= -3 - 2x⌊ -3 / 2 ⌋

= -3 - 2x⌊ -1.5 ⌋

= -3 - 2x(-2)

= -3 + 4 = 1

所以:

(-2) mod 12 = 12-2=10

(-4) mod 12 = 12-4 = 8

(-5) mod 12 = 12 - 5 = 7
再回到时钟的问题上:

回拨2小时 = 前拨10小时

回拨4小时 = 前拨8小时

回拨5小时= 前拨7小时

注意,这里发现的规律!

结合上面学到的同余的概念,实际上:

(-2) mod 12 = 10

10 mod 12 = 10

-2与10是同余的.

(-4) mod 12 = 8

8 mod 12 = 8

-4与8是同余的.

距离成功越来越近了. 要实现用正数替代负数, 只需要运用同余数的两个定理:

反身性:

a ≡ a (mod m)

这个定理是很显而易见的.

线性运算定理:

如果a ≡ b (mod m),c ≡ d (mod m) 那么:

(1)a ± c ≡ b ± d (mod m)

(2)a * c ≡ b * d (mod m)

所以:

7 ≡ 7 (mod 12)

(-2) ≡ 10 (mod 12)

7 -2 ≡ 7 + 10 (mod 12)

减去一个数,对于数制有限制,有溢出的运算(模运算)来说,相当于加上这个数的同余数

例子

先不引入符号位

0110(6)- 0010(2)

由于减去一个数,对于数制有限制,有溢出的运算(模运算)来说,相当于加上这个数的同余数,那么这个数是多少呢?从前面的运算可以看出这个数与减数相加正好等于模。
四位二进制数的模即是其最大容量2^4 = 16 = 10000B,于是2的同余数等于 10000-0010 = 1110(14),于是原式等于:

0110(6)- 0010(2) = 0110(6)+ 1110(14) = 10100(20)

此时结果为10100,由于是四位二进制数,最多只能存放4位,所以取低四位0100(4),正好是想要的结果。

减去2,从另外一个角度来说,也是加上(-2)。即加上(-2)和加上14其实得到的二进制结果除了进位位,结果是一样的。

如果我们把1110(14)的最高位看作符号位后就是(-2)的补码,这可能也是为什么负数的符号位是‘1’而不是‘0’。

在有符号位的四位二进制数中,能表示的只有‘-8~7’,而无符号位数(14)的作用和有符号数(-2)的作用效果其实是一样的。

带符号位的四位二进制补码:

- 正数 - 负数(补码) - 负数 (反码) - 负数(原码)
0 0000 0 0000 -0 1111 -0 1000
1 0001 -1 1111 -1 1110 -1 1001
2 0010 -2 1110 -2 1101 -2 1010
3 0011 -3 1101 -3 1100 -3 1011
4 0100 -4 1100 -4 1011 -4 1100
5 0101 -5 1011 -5 1010 -5 1101
6 0110 -6 1010 -6 1001 -6 1110
7 0111 -7 1001 -7 1000 -7 1111
- - -8 1000 - - - -

现在补码也不存在(-0)了,1000表示(-8)。

补码还可以这样画:

这也解释了当int(或其它类型)中存的数据大于自身所能容纳范围时,变成负数的现象。