39

深入浅出:举个🌰解读原码、反码和补码

 4 years ago
source link: https://studygolang.com/articles/24985
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

先来看一道 Go 语言中简单的运算题:

package main

import "fmt"

func main() {
        var a int8 = -128
        var b = a / -1
        fmt.Println(b)
}
复制代码

在 Go 语言中, int8 代表 有符号 8 位整数。你觉得输出结果是什么呢?我们在文末再公布答案,在此之前,我们先来回顾一下有符号整数是什么。

有符号整数

一个数在计算机中的 二进制表示 称为 机器数 ,这个机器数是带符号的。它的最高位是符号位,0 代表正数,1 代表负数。

以 8 位有符号整数为例, 0000 0001 代表十进制中的 1, 1000 0001 则代表十进制中的 -1。

meYnyim.png!web

那么,你可能会问了:这样一来,8 位有符号整数的可表达范围应当是 [ 1111 1111 , 0111 1111 ],即 [-127, 127],但实际上它的可表达范围却是 [-128, 127],那么 -128 又从何而来呢?

想要理解 -128 的来历,我们还要知道原码、反码和补码的概念。

原码、反码和补码

计算机需要使用特定的编码方式存储数据,原码、反码和补码都是一种特定的编码方式。以下示例均以 8 位二进制数举例。

原码

原码是「未经更改的码」,指一个 二进制数左边加上符号位 后所得到的码。

  • 当二进制数大于 0 时,符号位为 0
  • 当二进制数小于 0 时,符号位为 1
ZjIz6v3.png!web

因此,用这种编码方式表示有符号的 8 位二进制数,它的取值范围是 [ 1111 1111 , 0111 1111 ],即 [-127, 127]。

如果我们使用原码计算 (+1) + (-1) 会得到什么结果呢?

(+1) + (-1) = 

0000 0001(原码) 
+
1000 0001(原码)
=
1000 0010(原码) 
=
(-2)
复制代码

What? 等于 -2 了?这显然是错误的答案。

反码

为了解决「正负数相加」的问题,人类又发明了 反码

反码的表示方式为:

  • 正数的反码等于它的原码
  • 负数的反码则保留其原码符号位,然后对其他位进行取反操作

取反操作:将 0 变为 1,1 变为 0。

ZbiE7vE.png!web

此时,用反码来计算 (+1) + (-1)

(+1) + (-1) = 

0000 0001(反码) 
+
1111 1110(反码) 
= 
1111 1111(反码) 
=
(-0)
复制代码

我们知道,0 的原码是 0000 00001000 0000 ,0 的反码就是 0000 00001111 1111 。即在反码中, 1111 1111 象征 -0 ,我们终于求出了正确的结果。

补码

但反码的表示方法中存在着 +0-0 两个零,我们希望只有一个 0,所以 补码 [^3]出现了。

补码的表示方式为:

  • 正数的补码是其本身
  • 负数的补码是在它的反码基础上加 1
EBjequY.png!web

由于 +1 的操作,必将出现进位, 如果进位超过长度限制,最高位就会丢失

会发生最高位丢失的数就是 -0 的反码表示 1111 1111 ,它的补码为 1 0000 0000 。由于长度是 8 位,最高位的 1 已经溢出,所以丢弃, -0 的补码就成了 0000 0000 ,和刚才我们所提到的 +0 完美重合了!

用补码来计算 (+1) + (-1)

(+1) + (-1) = 

0000 0001(补码) 
+
1111 1111(补码) 
= 
0000 0000(补码) 
=
(0)
复制代码

答案正确!

因此,计算机内部使用补码方式表示负数,因为它让「正数 + 负数」也能使用同一套加法规则,使得所有的加法运算可以使用同一种电路完成。

-128 的由来

上面我们说到如果用补码的方式进行表示, -0 就不存在了,为了让有限的位数尽可能表示更多的数,省下的 1000 0000 就用来表示 -128 了。

让我们用补码来计算一下 (-1) + (-127)

(-1) + (-127) = 

1000 0001(原码)+
1111 1111(原码)
=
1111 1110(反码)+
1000 0000(反码)
=
1111 1111(补码)+
1000 0001(补码) 
=
1 1000 0000(补码)
=
1000 0000(丢弃最高位)
= 
(-128)
复制代码

(-1) + (-127) 的结果正是 -128。但由于 1000 0000-0 的补码,所以 -128 没有与之对应的原码和反码表示。

公布答案

好了,让我们再回头看一下开头的那段代码:

package main

import "fmt"

func main() {
        var a int8 = -128
        var b = a / -1
        fmt.Println(b)
}
复制代码

显而易见了: int8 的可表示范围是 [-128, 127] ,所以可以被赋值为 -128。而 (-128) / (-1) = 128 显然超过了该表示范围,+128 用有符号整数表示需要 9 位,表示为 0 1000 0000 ,最高位的 0 已经溢出,所以丢弃,导致结果是 1000 0000 ,即 -128。

所以这段代码的输出结果就是 -128 啦~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK