在计算机中,数字是用二进制来表示的,使用二进制的方式考虑到两点 1. 问题简单化 电路简单 2. 计算简单化 二进制特性使然。在计算机中 数字是有限位的这是与数学概念不同的地方,比如在32位系统中 char 类型表示1个byte 1个byte由8个bit组成,这8个bit的数字表达能力(能表示的不同状态)是 2^8 = 256 个,也就是说1个byte能表达的数字范围是 1 ~ 256 考虑到0的特殊性 需要把 0 也加进来 这时候的表达范围是 0 ~ 255 。对于自然数 存在负数的形式 需要将一半的数据拿来表达负数,这个时候1byte能表达有符号数字的范围是 -1 ~ -128 + 0 ~ 127 也就是 -128 ~ 127 ,这个是概念 在这些比特位上如何去实现这样的设想呢,目前的计算机中是将正数和零 正常存储(也就是存在低位)负数通过补码的形式存储(存储在高位) 且 补码的求法是 补码 = 反码 + 1

其实对于这个 补码 的来历以及求法很疑惑,并不清楚通过什么逻辑得到的,虽然这样的设计非常巧妙 这样的存储形式 可以直接用原码的形式进行简单的加法计算,这样也进一步降低的计算机逻辑电路的复杂度,通过补码这种存储形式 可以将所有的计算看作加法这一种形式。补码 在帕斯卡的机械计算器中也有用到,也许是第一次使用补码的形式把。

接下来我要一步步的分析下 前辈是如何发现并利用这样一个数字特性的(这并不是发明 这是数字存在的特性 我们是发现了这些特性并使用在了计算机上),8bit太多 这里简化问题 使用 3bit来分析。首先需要说明几个概念 ,既然我们是利用这些 bit 位实现对正负数的表达 就涉及到一些变化,对于最终在计算机上的存储形式 称作 原码 而这个bit组合状态 所表达的实际值 称作 表达值

3bit所能表达的状态如下:

1
2
3
  000    001    010    011    100    101    110    111
---+------+------+------+------+------+------+------+---
0 1 2 3 4 5 6 7 <--- 原码

考虑到负数的引入,这里需要拿出一半的状态用来表示负数,最先想到的方式 是 数学中数轴的表达方式,如下:

1
2
3
4
  000    001    010    011    100    101    110    111
---+------+------+------+------+------+------+------+---
0 1 2 3 4 5 6 7 <--- 原码
X -3 -2 -1 0 1 2 3 <--- 表达值

这时有一个位是没有用到的,可以先暂时忽略,关注主体。其实这里的数字是可以直接用来计算了,可以用来计算的原因是他们按照数学计算逻辑中的大小顺序排列的。其中 表达值 = 原码 - 状态总数/2 这样是可以直接通过原码进行计算的,证明过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假设:
状态总数 C
表达值1 x
表达值1的源码 x'
表达值2 y
表达值2的源码 y'

已知:
x = x' - C/2
y = y' - C/2

求:
x+y = x' - C/2 + y' - C/2
= x' + y' - 2(C/2)
= x' + y' - C

表达值的计算最终可以转换成原码的计算,但需要减去状态总数,这其实多了一步转换,且这些转换对于正数负数都需要,对这个排列进行优化 会想到将正数放在低位 负数放在高位,这样正数的表达值 = 原码 ,需要转换处理的是负数,这样得到如下结果:

1
2
3
4
  000    001    010    011    100    101    110    111
---+------+------+------+------+------+------+------+---
0 1 2 3 4 5 6 7 <--- 原码
0 1 2 3 X -3 -2 -1 <--- 表达值

这样的排列形式还是可以直接进行运算的,因为其核心 只是将正负数的连续数字部分做了偏移 这些偏移是可以通过等价条件相互转换的,分析这个转换过程:已知之前的转换条件是 表达值 = 原码 - 状态总数/2 我们需要将正数与负数的位置进行调换,这时候需要分开对正数与负数的变换过程进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
对于之前的表达值与原码的关系可得
表达值=原码 - 状态总数/2
原码=表达值+状态总数/2

已知
原码 = 表达值+状态总数/2

表达值的整体左移 `状态总数/2` 个位置 相当于 原码右移 `状态总数/2` 个位置 也就是 源码应该减去 `状态总数/2`
则有
原码 = 原码-状态总数/2
= 表达值+状态总数/2-状态总数/2
= 表达值
也就是说此次的移动 正数的表达值就是原码

同理得到负数的情况 负数是右移 `状态总数/2`
则有
原码 = 原码 + 状态总数/2
= 表达值 + 状态总数/2 + 状态总数/2
= 表达值 + 状态总数

最总得到转换后的 源码 与 表达值的关系
正数 表达值 = 原码
负数 表达值 = 原码 - 状态总数

上面已经说了,位置的调整不会影响到计算,我们也通过转换关系改变了 表达值 与 原码的转换关系,这个时候数字的可计算性是没有变化的,也就是说 通过这个转换过程我们就能通过表达值的计算转换成 原码的计算。

虽然,经过上一步的位移 我们减少了对正数的转换,但是负数还是需要转换,且增加了对正数负数情况的区别,看起来并没有简化问题。但是对于负数的情况我们需要单独分析下,表达式 = 原码 - 状态总数 这个公式 中的 原码 - 状态总数 其实就是 原码 其中的 - 状态总数 并无意义。这要说起计算机的数字是有限范围的 而数学中的数字是没有范围的(他是概念的形式存在的),对于数字有范围的计算机 当其值超出其所能表达的范围外的时候 这个计数会从 零 重新开始。

数学上的数轴是从无限小到无限大 , 计算机中的这个数轴更像是钟表的环形样式,超过十二点就是一点了。所以任何值 加上或减去 状态总数 其实就等于其本身。由于这样一个特性 我们可以将上面负数的转换过程进行简化调整,

1
表达值 = 原码 - 状态总数 = 原码

进而,对于正数、负数都成立的 表达式 = 原码,也就是 可以直接参与计算了 直接用原码进行数学元算即可!

这其中的负数那部分 原码 其实是被叫做 补码 的 , 他特指负数的原码,关于 补码 的推导过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假设
负数表达值 x
原码 x'
范围最大值 MAX
状态总数 C

已知
C = MAX + 1
MAX = -x + ^(-x)


x' = x + C
= x + MAX + 1
= x + (-x) + ^(-x) + 1
= ^(-x) + 1

最终得到 x‘ = ^(-x) + 1
由于 x 是负数 -x 是这个负数的绝对值 也就是正数 ^(-x) 也就是负数对应的正数的反码
原码 = 表达值绝对值的反码 + 1
也就是现在常说的 补码 = 反码 + 1