整型数值的二进制表达方式

众所周知,对 0 取反是 -1,对 -1 取反是 0;即 0-1 互为反码。
那么,为什么呢?因为「在计算机中,负数以其正值补码形式表达」。

初步认识#

原码#

举个例子,假设这里有一个 int32 的 1,用二进制形式(0/1 数据流)表示:

00000000 00000000 00000000 00000001

这叫做「原码」。

反码#

对其 按位取反~),得到:

11111111 11111111 11111111 11111110

这叫做「反码」。注意反码和原码是相互一一对应的,我们称之为「互为反码」。

补码#

将反码 + 1,得到:

11111111 11111111 11111111 11111111

这叫做「补码」。正如一开始所说的:1补码就是 -1(在计算机内部的表达形式)。

回收 flag#

那这里的「-1」1 的 补码按位取反是什么呢?

00000000 00000000 00000000 00000000

也就是 0。正如第二步所说的:0-1 互为反码

进一步深入#

如果 0 的反码是 -1,那 0 的补码又是什么?
这就要引入一个(我之前故意忽略的)新概念了:符号位。

无符号位#

众所周知,计算机运行的本质是输入输出(I/O)和操作 0 / 1 的二进制数据流。
大到 3D 游戏,小到一个小小的十进制数,在计算机眼里都是一样的 0 / 1

所以 4 字节Byte = 4 × 8 bit 的 int32 共占 32 bit。(这也是 32 位系统默认的 整型整数类型
理论上可以表示高达 $ 2^{32} - 1 = 4294967295 $ 的正整数,那负数怎么办?

注意这里说的「正整数」包括 0——确切地说,是正零(+ 0)。

有符号位#

好办,我们把最高位当作「符号位」就行了:0 代表正数、1 代表负数。

所以同样是 int32,unsigned 可以表达 0 ~ 4294967295
signed 直接砍一半正整数,表达 -2147483648 ~ 2147483647(没有写错,负数就是大 1)。

$$ 2^{32-1} - 1 = 2147483647 $$

计算机内部(真实)的二进制表达#

从下往上看(建议先从 + 0 开始),数值逐渐递增:

计算机内部真实的二进制值我们使用的十进制值
10000000 00000000 00000000 00000001- 2147483647(开始循环)
10000000 00000000 00000000 00000000- 2147483648(形式上视为 - 0
01111111 11111111 11111111 11111111+ 2147483647
中略……
00000000 00000000 00000000 00000011+ 3
00000000 00000000 00000000 00000010+ 2
00000000 00000000 00000000 00000001+ 1
00000000 00000000 00000000 00000000+ 0
11111111 11111111 11111111 11111111- 1
11111111 11111111 11111111 11111110- 2
11111111 11111111 11111111 11111101- 3
中略……
10000000 00000000 00000000 00000001- 2147483647

用一个字节(Byte)来打比方:
有符号位其实是把 [0, 255] 对半砍成 [0, 127][128, 255]
然后用 [128, 255] 来表示 [-128, -1](以补码形式)。

或者说「负数的表达方式([128, 255])」就是它([-128, -1])绝对值的补数。
什么补数?补到「模」的补数,模就是你永远无法达到的上限(与标准),此处指 256
再举个例子,时钟的模就是 12:

  • 你把 3 点的时针正拨 6,是 9 点
  • 你把 3 点的时针倒拨 6,还是 9 点

如果实在捋不清楚,可以这样理解:其实正负两边都是一样的——

  • +0 以及 +1 ~ +2147483647
  • -0 以及 -1 ~ -2147483647

但是 0 不需要两个,所以 -0 依据符号位拿去当作 -2147483648 用了。

有必要搞的这么麻烦吗?当然是有必要才会这么搞啦。
这样设计的目的是为了将减法转换为加法(减去 1 等于加上 -1)。
如此一来,就把 十进制的加法 完全转换成了 二进制的加法
更多细节可以自行了解「CPU 减法器」的实现原理。

回收 flag#

正零 + 0

原码:

00000000 00000000 00000000 00000000

反码:(正数的反码是其本身)

00000000 00000000 00000000 00000000

补码:(正数的补码也是其本身)

00000000 00000000 00000000 00000000

负零 - 0

原码:

10000000 00000000 00000000 00000000

反码:(负数的反码,符号位不变,其他位取反)

11111111 11111111 11111111 11111111

补码:(负数的补码,反码 + 1,超出上限的进位舍弃)

00000000 00000000 00000000 00000000

说到底,原码和反码是为了人类理解「补码」生造的概念。
知道有这么一茬就行了,不必强行记忆,硬背下来除了搞混脑子也没什么卵用。
计算机的事,不交给计算机自己解决,那造它干嘛?

lackbfun © 2021 - 2024