信息的表示(一)
现代计算机存储和处理的信息均以二值信号表示。对于人来说,十进制已经完全够了,但对于计算机来说,二进制会表现得更好,为什么可以参考《从编码到二进制》一文。不同的数字有着不同的含义,这个含义是我们人去定义的,计算机如何理解,需要人去告诉它。对于不同的编程语言,计算机会有不同的理解方式。在此我们主要讨论的是C语言在计算机中的表示,其他语言使用了类似的方式,只是存在细微差别。
大多数的计算机使用一个字节作为最小的寻址单位,一个字节是由8个位组成的。我们研究三种最重要的数字表示法,分别是无符号数,补码和浮点数。在本文中我们先讨论无符号数和补码。
无符号数
对于传统的非负数,我们可以很容易的将十进制转换为二进制,只需要有足够多的位来表示就可以了。
补码
上述提到了传统的非负数,那么如果是负数怎么办呢?如果在前面增加一个符号位会怎样?例如,最开始从左到右的第一位如果是1表示负数,0则表示正数。这似乎不是一个很好的办法,因为这增加了额外的开销,而且对于0,将会有+0和-0两种情况,这将对数字处理带来不必要的麻烦(实际上这也就是反码)。考虑这样一个事实,如果在无符号数中使用0减去1会得到怎样的结果。当然在我们看来,结果显然是-1,但在计算机中却得到了1111 1111 1111 1111 1111 1111 1111 1111,即32个1组成的串。因为当减出来的数是一个负数是,原先的被减数显然不足以被减,需要向前面借位,同时无符号数字的位数足够多的情况下则一定以0开头,而这也就导致一个事实 —— 负数一定以1开头。
这似乎是一个天然的规律,可不可以使用这个规律来表示负数呢?答案是可以的。将二进制串看成一个比特向量,例如1111 1111可以看成\([x_{w-1}, x_{w-2}, x_{w-3}, x_{w-4}, x_{w-5}, x_{w-6}, x_{w-7}, x_{w-8}]\),w表示长度,其中每一位均为1。在无符号数的表示中,每一个位乘上该位的权重,再加和即可得到最后的十进制非负数结果。而在补码中,第一位可以看作是一个符号位,但它也是带有权重的,这个权重是\(-x_{w-1}2^{w-1}\),也就是将无符号数原先该位的权重加上了负号得到的解。这样既没有多余的开销来表达负数也没有+0和-0的区别。
对比二者的转换
将无符号数x转换成十进制:
\[func(\vec {x}) \doteq \sum \limits _ {i=0}^{w-1}x_i2^i \]将补码x转换成十进制:
\[func(\vec{x}) \doteq -x _ {w-1}2^{w-1}+ \sum \limits_ {i=0}^{w-2}x_i2^i \]有了以上公式,很容易得到无符号数和补码十进制的转换公式
将无符号数转为补码:
$$func(x) = x - x_{w-1}2^w$$
将补码转为无符号数:
$$func(x) = x + x_{w-1}2^w$$
位级运算
布尔运算
布尔运算中有与或非三种最基本情况,这放在二进制表示数字的每一个位上同样适用。在C语言中,可以对一个数字求非,对两个数字求与和或,运算符分别是~、&、|。如果有布尔代数的知识基础在这里会很容易理解。
逻辑运算
有了基础的布尔运算还不太够,因为程序中一定会设计到大量的逻辑判断,是或者否。于是也就有了逻辑运算符&&、||、!,分别对应于命题逻辑中的AND、OR、NOT。
移位运算
移位运算也就是在位的角度,将位进行左移或者是右移。在C语言中,对位执行左移,将会在右边补上0。但对位执行右移,则存在了两种不同的情况,分别是逻辑右移和算数右移,C语言标准并没有明确规定使用哪种情况,但事实上几乎所有的机器都对有符号数字使用算数右移,对无符号数字均使用逻辑右移。我们来看一下这几种不同的情况。
操作 | 值1 | 值2 |
---|---|---|
参数x | 01100011 | 10010101 |
x << 4 | 00110000 | 01010000 |
x >> 4 (逻辑右移) | 00000110 | 00001001 |
x >> 4 (算术右移) | 00000110 | 11111001 |