整型数值的二进制表达方式
众所周知,对 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#
那这里的
00000000 00000000 00000000 00000000
也就是 0。正如第二步所说的:0 和 -1 互为反码。
进一步深入#
如果 0 的反码是 -1,那 0 的补码又是什么?
这就要引入一个(我之前故意忽略的)新概念了:符号位。
无符号位#
众所周知,计算机运行的本质是输入输出(I/O)和操作 0 / 1 的二进制数据流。
大到 3D 游戏,小到一个小小的十进制数,在计算机眼里都是一样的 0 / 1。
所以 4
理论上可以表示高达 $ 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
说到底,原码和反码是为了人类理解「补码」生造的概念。
知道有这么一茬就行了,不必强行记忆,硬背下来除了搞混脑子也没什么卵用。
计算机的事,不交给计算机自己解决,那造它干嘛?