2

深入浅出:举个例子解读原码、反码与补码

 2 years ago
source link: http://jalan.space/2019/11/26/2019/binary-show/
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。

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

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

原码、反码和补码

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

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

  • 当二进制数大于 0 时,符号位为 0
  • 当二进制数小于 0 时,符号位为 1

-127 和 127 的原码表示

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

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

(+1) + (-1) = 

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

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

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

反码的表示方式为:

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

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

原码 -> 反码

此时,用反码来计算 (+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

原码 -> 反码 -> 补码

由于 +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 啦~

  • int8 表示有符号 8 位整数,它的可表示范围是 [-128, 127]
  • 计算机内部使用补码方式表示负数
  • 补码解决了 +0-0 并存的问题,并省下 -0 的表示方法,多表示了一个最低数 -128
  • 补码使得所有整数集都能使用同一套加法规则
  • 如果发生溢出,多出的高位将被截取




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK