原码、反码、补码、移码和数值计算
欢迎访问我的个人站点,老廖的个人博客。
前言
计算机的数值编码和运算应该是本科一年级就会学习的基础知识。从软件开发这个角度来说,很多时候这些知识没有在开发过程中得到有效的利用和实践。
不巧,最近在做的一个项目,常常需要从补码的角度考虑数值表示和相关关系。因此,也就趁此机会简单的写一写。
数值表示
真值,机器数
机器数就是数值在计算机中的二进制表示,数值有正负之分,因此机器数用最高位来表示数值的符号,0 代表正数,1 代表负数。根据表示方法不同,机器数分为:原码、反码、补码、移码等。
真值就是带正负的真实十进制值。
原码
原码很简单,就是最高位作为符号位,其余位表示真值的绝对值。
反码
正数的反码就是 其原码本身 ,负数的反码则是 原码除符号位外 ,其余位取反。
反码的运算方法为 循环进位,即 最高位的进位要加到最低位来。如:8位为例:11111110(-1) + 11111110(-1) = 11111100 + 1 = 11111101(-2)
产生的原因: 计算机的所有计算本质上都是加法,然而若正负值相加时让计算机判断符号位来选择运算方式会使得加法电路设计变得复杂,但是若直接让符号位参与运算则会带来 1 + (-1) = -2 (8位为例:00000001 + 10000001 = 10000010)
等等问题。反码的提出就是为了解决 符号位参与运算 的加法问题。
补码
反码是一个不完美的解决方案,有不近人意的问题。比如: 8位为例,00000000 和 11111111
都可以表示零,一个 +0
,一个 -0
。为了解决这个问题,引入补码来表示数值。
正数 的补码是 其原码本身 ,负数 的补码是 其反码 + 1 。这个设计使得加法运算满足一个等式:a(补) + b(补) = (a + b)(补)
,由此,不管符号为何,直接参与运算都能得到正确的结果。目前补码是最佳的解决方案。 现行的编程语言,都是用补码来表示数值和进行算数运算
设计的原理:
设计补码的原因是要解决表示反码表示 0 的问题。
计算机表示数值是有范围的,取决于用多少 bit 来表示,当加法计算超过了 bit 的长度,就会产生溢出 ,溢出的部分消失,这和 模运算 恰好一致。比如上限是 255,则 5 mod 255 = 5,260 mod 255 = 5。
我们可以说,在8位的情况下,5 和 260 是等价的。同理,在负数的情况下 (反向理解),我们可以用负数的模除值等价负数,这样相当于用正数 (负数的模除值为正数) 表示了负数。
根据模运算的运算法则,(a+b) mod m = [(a mod m) + (b mod m)] mod m
,在 m
为最大值的情况下,数 mod m
的值就是 数
的反码。反码 + 1
等价于 m + 1
并不破坏等式,同时保证了符号的正确和 0
的正确表示。
移码
移码则是 把补码的符号位取反,常常用在 浮点 数值的二进制表示中。
浮点的二进制表示
浮点数的二进制表示比较特殊,整个二进制位分为三个部分:
类型 | 符号位 | 阶码 | 尾数 | 总位数 |
---|---|---|---|---|
float | 1 | 8 | 23 | 32 |
double | 1 | 11 | 52 | 64 |
设阶码为 e,尾数为 m,则浮点的值为: $ m * 2^e\quad $,其中阶码是用移码表示。
用移码表示的原因在于,阶码作为指数是有正负的,用移码表示能在 不考虑符号 的情况下比较浮点数大小。如:(8位情况下) 11111111 是最小值,00000000 是 0
如果不考虑符号,则 11111111 > 00000000
显然不方便。而用移码则:11111111 -> 00000001,00000000 -> 10000000
大小一下就比较出来了。
阶码确定浮点数的取值范围,尾数确定浮点数的精度。
数学运算
上面也说到了,计算机的数值运算是 通过补码完成的 。两个数的补码进行加法运算得到最终的值。若要显示成人类可读,则通过补码的定义转换成真值。
整型溢出
数值运算时极易发生整型溢出,直接通过补码的变化来判断溢出是否发生是非常准确的。
我们把最高位 (符号位) 紧挨着的那一位称为最高有效位,那么,若符号位和最高有效位只有一个发生了进位,则出现了整型溢出。