概念
计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用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点, 需要怎么做呢?我们可以:
-
往回拨2个小时: 6 - 2 = 4
-
往前拨10个小时: (6 + 10) mod 12 = 4
-
往前拨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 | -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(或其它类型)中存的数据大于自身所能容纳范围时,变成负数的现象。