原码 反码 补码
对于 原码、反码、补码的学习记录
对于概念的描述也是基于个人理解做出的易于理解的表达方式,没有使用更严谨的数学公式定义只针对整数
只针对加法个人是从
高位为符号位+低位为数值位
的角度去理解的
但是不引入符号位这个概念也完全可以理解这三个概念,那就是另一种表述了,没有深入了解非科班出身,足够个人使用的个人理解,可能根本原理上会有理解错误,如有错误希望有大佬指正
1.机器数和真值
1.1 机器数
数字在计算机中的二进制表示形式,有以下两个特点
- 具有符号位,即二进制中的最高位也就是最左边的一位表示符号, 0为正数, 1为负数(这是单符号位,还有双符号位不考虑)
- 其大小受字节长度限制
例如, 在 Java 的 byte 类型(1个字节Byte,8个比特bit)中
1 = [0000 0001]
-1 = [1000 0001]
1.2 真值
真值就是机器码对应的真正的值,可以简单理解为使用正负号代替符号位
1 = [0000 0001]
真值为 +000 0001
-1 = [1000 0001]
真值为 -000 0001
2.原码
2.1 定义
一个数的原码为
符号位
加上真值的绝对值
即最高位为符号位,剩余位(数值位)为真值绝对值
.
原码中0
会出现两种表示方式1000 0000
和0000 0000
即-0
,+0
原码是人可以直观理解的表示方式
以 Java 的 byte 类型(8bit,最高位为符号位,剩余7位为数值位)来说
原码取值范围为 -127 ~ 127
, 存在符号位也参与计算, 0000 0000
和 1000 0000
只能都指向 0
1 = [0000 0001]
: 正数,最高位为0
, 数值位为1(真值1的绝对值
)的二进制 000 0001
-1 = [1000 0001]
: 负数,最高位为1
, 数值位为1(真值-1的绝对值
)的二进制 000 0001
2.2 加法运算
一般计算机中只有加法器而没有减法器,若设置单独的减法器则需要考虑结果的符号等一系列其他问题
为了简化设计,最简单的思路就是把减法转换为加法
1 + 1 = [0000 0001]原 + [0000 0001]原 = [0000 0010]原 = 2
-1 + (-1) = [1000 0001]原 + [1000 0001]原 = [1 0000 0010]原 = -2 // 溢出了
-1 + (-2) = [1000 0001]原 + [1000 0010]原 = [1 0000 0011]原 = -3 // 溢出了
-1 + 1 = [1000 0001]原 + [0000 0001]原 = [1000 0010]原 = -2
-1 + 2 = [1000 0001]原 + [0000 0010]原 = [1000 0011]原 = -3
-0 + 0 = [1000 0000]原 + [0000 0000]原 = [1000 0000]原 = -0
可以看到, 正数相加和负数相加是没问题的,而正数+负数则会出现问题
其实原码也是可以通过一系列的运算规则来得到正确的结果的,但是需要额外判断符号位、溢出等情况,这样一来设计复杂程度将提高很多
3.反码
3.1 定义
正数反码是其自身
负数反码为原码的符号位不变
,数值位按位取反
8位二进制反码取值范围为 -127 ~ 127
, 存在符号位也参与计算但需要循环进位, 0000 0000 = +0
1000 0000 = -0
1 = [0000 0001]原 = [0000 0001]反
-1 = [1000 0001]原 = [1111 1110]反
3.2 加法运算
反码的加法涉及到一个 循环进位
的概念
简单理解就最高位即符号位计算时如果发生进位则需要将进位送到最低位再次相加
加法运算后结果仍为反码,但如果符号位为0则可以直接当做原码
1 + 1 =
0000 0001 // 原/反
+ 0000 0001 // 原/反
------------
= 0000 0010 = 2 // 符号位为0为正数,直接当原码
-1 + 1 =
1000 0001 // 原
+ 0000 0001 // 原
------------
= 1111 1110 // 反
+ 0000 0001 // 反
------------
= 1111 1111 // 符号位为1为负数,仍为反码
= 1000 0000 = -0
-1 + (-1) =
1000 0001 // 原
+ 1000 0001 // 原
------------
= 1111 1110 // 反
+ 1111 1110 // 反
------------
= 1 1111 1100 // 符号位进位了
= 1111 1101 // 循环进位 符号位为1为负数,仍然是反码
= 1000 0010 = -2
-1 + 2 =
1000 0001 // 原
+ 0000 0010 // 原
------------
= 1111 1110 // 反
+ 0000 0010 // 反
------------
= 1 0000 0000 // 符号位进位了
= 0000 0001 = 1 // 循环进位,符号位为0为正数,直接就是原码
-0 + 0 =
1000 0000 // 原
+ 0000 0000 // 原
------------
= 1111 1111 // 反
+ 0000 0000 // 反
------------
= 1111 1111 // 结果仍然是反码
= 1000 0000 = -0
0 + 0 =
0000 0000 // 原/反
+ 0000 0000 // 原/反
------------
= 0000 0000 = +0 // 原/反
可以看到,反码的运算可以满足减法变换为加法,但是运算规则较为复杂(当前少量设备是基于反码进行运算的,没有深入了解)
仍然不能忽略 +0
和 -0
4.补码
4.1 定义
补码的定义使用了
模
和同余
的概念。
正数的补码即为原码。
负数的补码等于模 + 真值
,其中模 = 2数值位数+1(也即可以表示数据的数量,双符号位(00为正,11为负)的同样遵循该方式)
根据定义,可以得到一个结论 负数补码 = 真值 + 模
-> 负数补码 = 模 - 真值绝对值
补码的意义是 按照这样一种编码规则(也就是表示正数、负数和零的约定),可以无需额外操作把减法运算变成加负数形式的加法运算
从而在直接省略减法器的同时简化加法器的设计
- 1.根据定义进行负数补码计算
对于Java的byte类型, 数值位数=7,则模的值为 27+1 = 256, 即byte类型可以容纳的总数据量
256 = [1 0000 0000]原
-1 = [1000 0001]原 真值 = [-000 0001]
-2 = [1000 0010]原 真值 = [-000 0010]
256 + (-1) =
1 0000 0000 // 模256 1 0000 0000
- 000 0001 // 真值 -000 0001
-------------
1111 1111 // -1的补码
256 + (-2) =
1 0000 0000 // 模256 1 0000 0000
- 000 0010 // 真值 -000 0001
-------------
1111 1110 // -2的补码
- 2.常用的一种通过反码计算负数补码的方法
负数补码 = 原码符号位不变数值位按位取反 + 1 = 原码的反码 + 1
负数原码 = (补码 - 1)后符号位不变数值位按位取反
.
在二进制中,取反加一
和减一取反
是等价的, 这点也是有证明的,有兴趣可以看一下
.
因此负数补码还原成原码的计算可以变换成
负数原码 = 补码符号位不变数值位按位取反 + 1 = 补码的反码 + 1
可以看到, 这个过程和原码->补码
的计算过程是一样的
.
因此我们可以得出一个比较绕的结论
补码的补码就是原码
或者补码和原码互为补码
,严谨点说是补码经过取反+1的操作后就是原码
这也是由
取模同余
这一概念决定的
-1 = 1000 0001 // 原码
1111 1110 // 反码
1111 1111 // 反码+1,即补码
-2 = 1000 0010 // 原
1111 1101 // 反
1111 1110 // 反码+1,即补码
注意
搜集资料时看到很多对反码的误解
虽然补码可以通过
反码+1
的方式计算
但并不代表反码就是原码和补码之间的过渡概念
也不代表补码是在反码的基础上发展而来
补码是根据取模同余
设计出来的
反码和补码拥有同样的地位
反码也是有实际应用的,并不是补码的工具人
只是恰好补码 = 反码 + 1
,而且这样也方便理解,计算机计算补码也是这个方法(使用定义来计算补码需要减法运算,而补码就是为了干掉减法)
数学上有对于补码 = 反码 + 1
的详细证明
4.1.1 模和同余
当两个整数除以同一个正整数,若得相同余数,则二整数同余
或者
两个整数 a,b,若它们除以整数 m 所得的余数相等,则称 a,b 对于模 m 同余
.
记作a ≡ b (mod m)
读作a 与 b 关于模 m 同余
需要注意 这里的余数应该使用 取模
运算获取,即计算中商应该向下取整
Java/C 等语言的
%
运算符向零取整
,即使商更接近0
, 应该叫做取余
Python 等语言的%
运算符向下取整
,即使商更接近-∞
,是取模
.
两种取整方式在正数计算中没啥差别,但是负数计算结果就不一样了
因此Java/C的%
运算符不能用于计算这个余数
Java中可以使用Math.floorMod()
来进行取模
运算
-23 mod 12 == 1 // 商≈-1.92, 向下取整=-2, 则余数为 -23 - (-2) * 12 = -23 + 24 = 1
-11 mod 12 == 1 // 商≈-0.92, 向下取整=-1, 则余数为 -11 - (-1) * 12 = -11 + 12 = 1
1 mod 12 == 1
13 mod 12 == 1
25 mod 12 == 1
则 -23,-11,1,13,25 都关于模 12 同余
这一点可以借助钟表表盘来理解
-23点
-11点
1点
13点
25点
在表盘上都是同一个位置,离12点
都只有1格
,只是顺时针/逆时针转了几圈的问题这也是补码运算不需要关心符号位的原因
由于取模同余
的性质,补码的取值范围(数轴)是圆形的
,超限了也只是进入下一圈或上一圈
而原码和反码这类直线数轴
的,超限后就完蛋了虽然没有
-23点
的说法,理解含义即可
4.2 加法运算
4.2.1 同余加法
补码的加法运算其实是依据以下性质进行的同余加法
如果 a ≡ b (mod m), c ≡ d (mod m) 那么:
a ± c ≡ b ± d (mod m)
同样的乘法也有类似的性质
a * c ≡ b * d (mod m)
例如 对于byte类型 -1 ≡ 127 (mod 128)
1 ≡ 1 (mod 128)
则 -1 + 1 ≡ (127 + 1) (mod 128) ≡ 128 (mod 128)
; 即 -1 + 1
与 128
对于模 128
同余
而 128 mod 128 = 0
; 则 -1 + 1
的值应该是byte类型取值范围(-128~127)中对128取模=0的数
,也就是0
对于byte类型(8bit二进制数), -128 比较特殊
- 补码取值范围为
-128 ~ 127
,存在符号位参与计算且不需用额外操作,也即补码的8位都可以作为数值位 - 解决了
+0
-0
的问题,因此相比于原码和反码空出了-0
即1000 0000
的位置
这个多出来的 1000 0000
用来干啥呢?
-
根据 4.1 补码定义,
-128的补码 = -128 + 256(模) = 128
数学上128的二进制表示为1000 0000
-
根据补码的连续性,8位二进制总共可以表示256个数字
根据补码表,发现按顺序向下推进,-128处的补码正好也是1000 0000
原码 反码 补码 真值 0111 1111 0000 0000 0111 1111 +127 0111 1110 0000 0001 0111 1110 +126 原码-1… 反码-1… 补码-1… 真值-1… 0000 0000 0111 1111 0000 0000 0 1000 0001 1111 1110 1111 1111 -1 原码-1… 反码-1… 补码-1… 真值-1… 1111 1111 1000 0000 1000 0001 -127 8位无法表示 0111 1111(实际没有反码) 1000 0000 -128
上面两个情况计算出来的 -128
补码都是 1000 0000
, 正好也是补码中多出来的位置
因此将 -128 的补码规定为 1000 0000
完全是合情合理的
而且 对应的原码在8bit下无法表示, 反码与 0 反码相同,所以 -128 没有原码和反码
可以发现,即使没有原码和反码,补码也是可以存在的
从这一点也可以得出,补码本质上是一个独立的概念,它不是以原码或者补码为基础存在的概念
原码、反码、补码三者是为了解决不同问题分别发展出来的三个独立概念
在数学上不能以概念出现的顺序来作为概念间关系的依据
感兴趣可以看看对数
和指数
两个概念的发展历史以及两者关系
4.2.2 补码加法
与反码的循环进位不同,补码的加法运算无需关注符号位的进位,可以将符号位的进位直接舍弃
+1 = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
-1 = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
+2 = [0000 0010]原 = [0000 0010]反 = [0000 0010]补
0 = [0000 0000]原 = [0000 0000]反 = [0000 0000]补
1 + 1 =
0000 0001 // 原/补
+ 0000 0001 // 原/补
-------------
= 0000 0010 = 2
-1 + 1 =
1111 1111 // 补
+ 0000 0001 // 原/补
------------
= 1 0000 0000 // 溢出,直接抛弃
= 0000 0000 = 0
-1 + 2 =
1111 1111 // 补
+ 0000 0010 // 原/补
------------
= 1 0000 0001 // 溢出,直接抛弃
= 0000 0001 = 1
-1 + (-1) =
1111 1111 // 补
+ 1111 1111 // 补
------------
= 1 1111 1110 // 溢出,直接抛弃
= 1111 1110 // 补
= 1000 0001 // 之前提过 补码的补码就是原码
= 1000 0010 = -2
5.总结
可以看到,计算机中大量应用了数学中的概念,可以说计算机就是在数学的基础上才发展起来的
只可惜没学好数学,更没学过计算机领域相关的数学概念,导致现在稍微想深入学习就得一点一点的慢慢抠
还有可能出现理解错误, 真是太难了