原码 反码 补码

机器数和真值

机器数

一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1 。

比如,十进制中的数+3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011

那么,这里的 0000001110000011 就是机器数。

真值

因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值13110000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。

因此10000011的真值为-3

原码 反码 补码

原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:

[-1] = 1000 0001

[+1] = 0000 0001

第一位是符号位。因为第一位是符号位, 所以8位二进制数的取值范围就是:

[1111 1111 , 0111 1111]

[-127, 127]

反码

反码的表示方法是:

  • 正数的反码是其本身

  • 负数的反码是在其原码的基础上,符号位不变,其余各个位取反:

    [+1] = [00000001] = [00000001]

    [-1] = [10000001] = [11111110]

可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值。通常要将其转换成原码再计算。

补码

补码的表示方法是:

  • 正数的补码就是其本身

  • 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后加1(即在反码的基础上加1)

    [+1] = [00000001] = [00000001] = [00000001]

    [-1] = [10000001] = [11111110] = [11111111]

为什么需要反码和补码

首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域的加减。 但是对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单 。 计算机辨别”符号位”显然会让计算机的基础电路设计变得十分复杂 。 于是人们想出了将符号位也参与运算的方法 。

我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1 - 1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了 。

对于计算1-1=0,首先来看原码:

1 - 1 = 1 + (-1) = [00000001] + [10000001] = [10000010] = -2

如果用原码来进行计算,将符号位也参与计算,显然计算结果是错误的。

因此引入了反码:

1 - 1 = 1 + (-1) = [00000001] + [111111110] = [11111111] = -0

发现使用反码计算,结果的真值是正确的。但是也存在问题,再看下面一个例子:

0 + 0 = [00000000] + [00000000] = +0

对于结果0,可能会得到-0+0两个不同的结果。

而补码的出现,解决了这个问题:

1 - 1 = [00000001] + [11111111] = [00000000] = 0

0 + 0 = [00000000] + [00000000] = 0

可以看到,使用补码进行计算,将会只有一个0,而且可以用10000000来表示-128:

-1 + -127 = [11111111] + [10000001] = [10000000]

因此,对于一个8位的带符号数,如果使用补码来进行表示,则取值范围位[-128, 127]

而对于原码和反码,因为都存在两个0

+0 = [00000000] = [00000000]

-0 = [10000000] = [11111111]

有效取值范围都只有[-127, 127]

使用补码,不仅解决了减法的问题,还修复了存在两个0的问题,使得负数的有效值可以多一位。

因此,计算机都是使用补码来表示正数,对于n位的整数,其有效范围为:

[-2n-1, 2n-1-1]

补码的本质

补码本质是用来构成一个环,以实现一个同余运算。

假设8位的整数为例,我们计算的结果最终都是2^8的余数,这与符号位没有关系,也就是需要mod 2^8,也就是当计算结果超过8位时,高位会被舍弃,就是溢出。

比如:

1 + 255 = 256 = 0

1 - 1 = 0

这其实是同余的概念:两个整数a,b,若它们除以整数m所得的余数相等,则称a,b对于模m同余,记作a ≡ b (mod m)

我们来看 255和-1:

255 mod 256 = 255

-1 mod 256 = 255

也就是 -1 ≡ 255 mod 256

在一个环中,00000000b和11111111b连接,当00000000b向后走一步,就回到11111111b了。

同余加法:

a ≡ b mod m

c ≡ d mod m

a+c ≡ b+d mod m

所以有:

1 ≡ 1 mod 256

-1 ≡ 255 mod 256

1-1 ≡ 1+255 mod 256

所以 1-1 = 1+255 = 0

对于255的二进制表示:

255 = 11111111b

因为 -1≡255 mod 256,在8位的带符号数中,我们可以用11111111b来表示-1,而这不正是-1的补码吗。

应用

前面讲到,计算机中使用补码存储整数,而我们可以把补码看成是一个环
因此:

1
2
3
a := uint8(2)
b := uint8(255)
a - b == 3 // 2倒退3步回到255

利用这个性质,当我们在实现环形队列时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type RingQueue struct {
buf []interface{}
head, tail uint32
cap uint32
}

func New(cap uint32) *RingQueue {
return &RingQueue{
buf: make([]interface{}, cap),
cap: cap,
}
}

func (r *RingQueue) Push(v interface{}) bool {
if r.tail-r.head < r.cap {
r.buf[r.tail%r.cap] = v
r.tail++
return true
}
return false
}

func (r *RingQueue) Size() uint32 {
return r.tail - r.head
}

func (r *RingQueue) Cap() uint32 {
return r.cap
}

func (r *RingQueue) Pop() (interface{}, bool) {
if r.tail-r.head > 0 {
v := r.buf[r.head%r.cap]
r.head++
return v, true
}
return nil, false
}

参考