整型数值的二进制表达方式
众所周知,对 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
说到底,原码和反码是为了人类理解「补码」生造的概念。
知道有这么一茬就行了,不必强行记忆,硬背下来除了搞混脑子也没什么卵用。
计算机的事,不交给计算机自己解决,那造它干嘛?