我一直想为什么计算机中一定要规定有反码、补码?原码不能解决计算机的计算能力吗?反码,补码的出现解决哪些问题?
带着这个问题,我对计算机知识进行脑补。
原理
因为计算机的一切都是通过0和1来表示,也就是二进制。而数值又分为有符号数和无符号数,无符号数理解起来,则要相对简单一些,没有符号位,即所有的二进制位都参与值计算,也就是说无符号数表示的都是正数,比如c语言中的unsigned int。但是现实当中,数值是分有正数与负数区分。在计算机当中0表示正数,1表示负数。往往一串二进制表达数值时,一般取最高为的数来表达正负。无论unsigned int还是有符号的数,计算过程中都是在数值范围内进行计算才得到正确的值。
有符号数:数值的最高位表示正负,1表示负,0表示正数,以8位的二进制数为例,10001010表示负数-10,而00000101则表示正数5;
无符号数:最高位是值的一部分,比如10001010换算成10进制就是138。
而有符号数的表示有3种方式:原码、反码和补码。
这里我们先只给出原码概念:
原码(十进制转二进制的值):即符号位用 0 表示正数,而用 1 表示负数,其他位表示数值本身。比如:10001010表示-10,00000101表示+5。
了解计算机底层计算逻辑
- 计算机CPU硬件设计,CPU是没有减法运算器,两个数相减,a-b=a+(-b) 其中a为正数。
- 数学中,数可以无限大,也可以无限小,但计算机容量有限,表示不了。故每一种数值类型的都是在一个区间之内的,即:[c, d],其中c表示下限,d表示上限。
下面代码:addend+addend2是溢出。计算结果是不正确
func main() {
//len计算字符串占用字节长度
var addend int8 = 100
var addend1 int8 = 3
var addend2 int8 = 32
fmt.Println("addend与addend1相加结果:", addend+addend1)
//addend+addend2 这两个相加是溢出
fmt.Println("addend与addend2相加结果:", addend+addend2)
}
运行结果:从下面结果来看,-124是错误的结果,为什么这样?因为golang中的int8是一个范围[-128~127]。在这个范围内的结果都是可以正确计算。超过这个范围就溢出,出现错误结果。
上面两个例子说明了计算机数值计算的范围重要性。是不可逾越的障碍。数值类型表示的数值有一定的范围,那么计算结果超出了这个范围,会发生什么呢?答案是:溢出。
现在我来解释100+32=-124的错误结果。
100与32在计算机的二进制如下图
100+32结果为1000 0100,这个是一个负数值。按照这个负值应该是-4,而不是-124。为什么出现这样结果?
那么我们就需要了解数值在计算机当中的存储方式。所有数值在计算机存储与表示都是以补码的方式出现。
原码、反码、补码之间的关系(以8位int8为例子)
原码:就是十进制转换为二进制的真实数据。例如正数10转换为二进制就是
0000 1010.
反码:反码规定正数的原码与反码相同。正数10的反码也是0000 1010。负数的反码是对正数逐位取反,0变1,1变0,符号位保持为1。例如-10的原码是1000 1010,那么它的反码是1111 0101
补码:补码规定正数的原码与补码相同。正数10的补码也是0000 1010。-10的补码在反码基础在加1。-10的反码是1111 0101,再加1.结果为1111 0110。
现在返回来看看上面100+32=-124的问题。在实际二进制相加过程中得到结果是1000 0100。由于溢出导致两个正数相加出现最高位为1,也就是负数。由于负数在计算机当中的表示与存储都是以补码方式存在。于是计算机会对1000 0100这个负数结果转换为补码形式得到是1111 1100(也就是测试结果的-124)
*正数的反码、补码与原码相同。反码、补码只针对负数进行转换。
为什么负数一定要用补码形式出现?
但是原码可以解决我们的负数运算问题吗?我们来试试:
// 计算: 5 + (-3)
0000 0101 // 5的原码
+ 1000 0011 // -3的原码
------------
1000 1000 // -8
可以看到:5 + (-3) = -8,这是错误的。所以用原码来表示负数,并进行运算是不行的。
我以10十进制的数来试解释一下补码:
9-3=6
9+7=16,这个是在数学计算结果,但是在10进制(假设计算机计算长度为0-9范围内),那么9+7=16的计算机计算中,会把溢出的1去掉,保留6.意味着9+7=6。这是计算机计算的正确结果。之前我们讲过计算机CPU是没有减法运算器。我们利用范围溢出方式进行减法算运,9-3=6和9+7=6,情况下,我们把-3在计算机中,用7来代替做加法运算。7就是-3的补码。关于计算机二进制中的负数补码转化方式,上面已经讲过。这个解释不一定科学,但是想不到有更加通俗的说法了。
// 计算: 5 + (-3)
0000 0101 // 5的原码
+ 1111 1101 //-3的原码1000 0011,反码是1111 1100,再加1得补码1111 1101
------------
10000 0010 // 左边溢出1,超过值范围了。省略掉1.取值为0000 0010,这值为
正数2.假如为负数,需要再转过为补码保存结果。
//我们来看看这个例子 计算: 5 + (-6)
0000 0101 // 5的原码
+ 1111 1010 //-6的原码1000 0110,反码是1111 1001再加1得补码1111 1010
------------
1111 1111 // 这里没有溢出。但是是一个负数,取反1000 0000再加1得补码
1000 0001 这是补码值-1,也就是我们真正的数学科学运算结果。
看一张图:以4位的二进制有符号数为例,对应的取值范围是:[-8, 7],我们将其想象成一个圆。来感受一个环形范围溢出结果。
网络图片
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7 // 上限
1000 -8 // 下限
1001 -7
1010 -6
1011 -5
1100 -4
1101 -3
1110 -2
1111 -1
0000 0 // 回到了0
...
下溢出,-8 + (-1) --> 1000 + 1111 = 0111,也就是上限7。
补充:移码
移码从何而来
知识点:
- 大家知道浮点数的组成是:符号位+阶码+尾数。
- 比较整数的方式:从高位到低位,逐位比较。
- 负数的原码,补码,反码的机器数都比正数大。
要知道
- 在cpu内,电路越简单越好。
- 而浮点数的运算经常有比较接码大小这种操作。
- 阶码只有整数,而通常定点整数的比较方式:数值位就是从左往右逐位比较。然而,无论阶码采用补码、原码都不行,因为无论补码还是原码,负数机器码都比正数大。
- 为了复用电路,采取比较定点数的方式来比较解码,于是设计了一种编码,真值和机器码是正比关系,由此引出了移码。
如何设计移码
要真值和机器时正比,很简单,高中数学告诉我们,平移就好了。
那平移多少呢?
先看三个位能表示的真值:
111 、110、101、100、011、010、001、000
我们在这些数中找到中间值作为0,如100。
可以类推为 2^(n-1),或首位为1其他位为0的机器数 作为数字0.
111 => 3
110 => 2
101 => 1
100 => 0
011 => -1
010 => -2
001 => -3
000 => -4
那么n位定点整数,可知:x移 = x+2^(n-1) (-2^(n-1) <= x < 2^(n-1))
由于 阶码不可能是小数,所以移码也不考虑小数。
移码的性质
- 0是唯一的。
- 符号位 1表示正,0表示负。
- x表示范围:(-2^(n-1) <= x < 2^(n-1)
- 移码和真值呈线性正比关系。
2^(n-1)就是符号位,也就是说,移码等于补码符号位取反,同理,补码符号位取反就等于移码。
2^(n-1)就是符号位,也就是说,移码等于补码符号位取反,同理,补码符号位取反就等于移码。
参考文档:https://www.jianshu.com/p/411cab22a71e