Issuu on Google+

计算机中有符号数存储方式和运算 ――原码、反码、补码 Yafei

Super Xu

2013 年 4 月 15 日 参考文章: http://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode. html https://zh.wikipedia.org/wiki/%E6%9C%89%E7%AC%A6%E8%99%9F%E6% 95%B8%E8%99%95%E7%90%86# 下面这个C语言的小例子大家知道答案吗? int main ( int argc , char ∗ argv [ ] ) { char v a l u e = −128; char z e r o = 0 ; p r i n t f ( ” z e r o −v a l u e=%d\n” , ( char ) ( z e r o − v a l u e ) ) ; return 0 ; } 也许你也不假思索的认为输出是128,那就错了!正确的输出: 0 − value = −128 为啥? 因为有符号char型变量的数值范围[127, −128], 上面的运算会造 成值溢出。为了探究这个问题的根本原因,我们需要弄明白计算机是如何 1


2 对数字的编码方式,并且计算机硬件对数学运算又是如何处理。下面让我 们一起揭开这个谜.

引言 0.1

为什么会出现原码,反码,补码? <1> 在计算机中,对于一个数,计算机要使用一定的编码方式进行存

储。而原码, 反码, 补码是机器存储一个具体数字的编码方式。 <2> 在数学中,负数都在最前面加上“-”符号来表示。然而在计算机 硬件中,数字都以无符号二进制形式表示,因此需要一种编码负号的方法。 而当前有四种方法来表示有符号数:原码,反码,补码,以及移码。(移码 好像很少见,在此我们主要讨论原码,反码和补码三种编码方法。)

0.2

如何表示有符号数的符号? 为了解决数字的符号问题,首先想到的解决办法是分配一位(bit)来表

示符号,通常用最高位作符号位,当该位为0时表示正数,为1 时表示负数。 所以一个字节(8位)中,最高位作为符号位,剩下的7位指示数值。

0.3

计算机硬件如何进行数学运算? 首先,因为人脑可以知道第一位是符号位,在计算的时候我们会根据

符号位,选择对数值区域(除去符号位的数值)加减。对于计算机, 加减乘 除已经是最基础的运算,要设计的尽量简单。计算机辨别”符号位”显然会 让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参 与运算的方法. 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即:1 − 1 = 1 + (−1) = 0, 所以机器可以只有加法没有减法, 这样计算机运算 的设计就更简单了.当然也有计算机硬件同时拥有加法器和减法器目 目前 计 算


3 机 硬 件 一 般 都 只 有 加 法 运 算, 没 有 减 法。 所 以 下 面 讨 论 三 种 编 码 对 负 数 的运 算 都 以 加 法来 运 算!

原码 原码是人脑最容易理解和计算的表示方式。因此对于一个8位有符号 数,除去最高位的符号位, 其数值范围[-127, +127],共255个数值。然 而8位共有256个编码,显然编码个数和前者的数值个数不匹配。 原码方式下,0有两种编码: [0000 0000]原 = +0 [1000 0000]原 = −0 原码的数学运算又是如何呢? 例: 1 + 1 = [0000 0001]原 + [000 0001]原 = [000 0010]原 = 2

正确

1 − 1 = 1 + (−1) = [0000 0001]原 + [1000 0001]原 = [1000 0010]原 = −2 错误 显然,原码在对负数运算处理不正确。因此,为了解决负数运算的问 题,反码诞生了,接着看。

反码 反码的编码方法如下: 正数[+0, +127]的反码是其本身(即和原码相同)。 负数[−0, −127]的反码是保持符号位不变,其余各位取反。虽然-0、 +0都表示0,但编码不同,姑且将-0其归并到负数中。


4 同样,反码中0也有两种编码: [−0] = [1111 1111]反 [+0] = [0000 0000]反 此时[1000 0000]反 是由[1111 1111]原 (−127) 转换得到,因此 [1000 0000]反 = -127。 在原码一节曾提到过,反码的诞生是为了解决负数的运算问题,那么 下面一样举例说明。 例: 1 − 1 = 1 + (−1) = [0000 0001]反 + [1111 1110]反 = [1111 1111]反; [1111 1111]反 的数值是多少呢? 我们转换成人脑能看懂的原码: [10000 0000]原 = −0; 我们发现反码计算的结果只是数值区域是正确的,而唯一的问题其实 就出现在”0”这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带 符号是没有任何意义的. 而且会有[0000 0000]原 和[1000 0000]原 两个编码表 示0. 于是补码出现了, 并且成功解决了0的符号以及两个编码的问题。

补码 正数的补码是其本身(即和原码相同),综上所述可以得出:正数的原 码,反码,补码相同。 负数的补码是在原码基础上,符号位保持不变,其余各位取反再+1。(即 在反码基础上+1) 既然补码解决在原码和反码中出现的问题,那么补码对上面的1 − 1的 例子运算又如何?


5 1 − 1 = 1 + (−1) = [0000 0001]补 + [1111 1111]补 = [0000 0000]补 = 0; 这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以 用[1000 0000]表示-128: (−1) + (−127) = [1111 1111]补 + [1000 0001]补 = [1000 0000]补; (−1) + (−127)的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是请注意实际上是用以前的-0的补码来表示-128, 所以-128并 没 有 原 码 和 反 码 表 示。 (对-128的 补 码 表 示[1000 0000]补 算 出 来 的 原 码 是[0000 0000]原 , 这是不正确的) 使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够 多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围 为[−127, +127], 而使用补码表示的范围为[−128, 127]。因此,计算机用补 码来存储数字。 小 贴 士: 如 何 快 速 在 补 码 和 数 值 之 间 转换? 对于正数,补码所见即所得,原码、反码、补码都一样,我们相信你 能快速转换。 对于负数,符号位为1,补码所见非所得,你的大脑需要经过转换才能 得到真实的数值。但是转换也是有技巧的,下面宽度为1个字节的负数为 例: [1000 0001]补 = [1000 0000]补 + [0000 0001]补 = −128 + 1 = −127 [1000 1000]补 = [1000 0000]补 + [0000 1000]补 = −128 + 8 = −120 这样拆分开来是不是很方便? 上面提到过了[1000 0000]补 这个特殊的 补码代码数值-128,所以对于负数,咱们拆分成-128再加上一个正数。 讲到这,我们相信你已经能把文章开头的问题解决了!不过我还是把 计算公式写在下面供大家参考: int main ( int argc , char ∗ argv [ ] ) {


6 char v a l u e = −128; char z e r o = 0 ; p r i n t f ( ” z e r o −v a l u e=%d\n” , ( char ) ( z e r o − v a l u e ) ) ; return 0 ; } 0−(−128) = 128 = 127+1 = [0111 1111]补 +[0000 0001]补 = [1000 0000]补 由于value和zero是有符号char型变量,所以[1000 0000]补 = −128。 如果将(zero - value)运算结果上升为无符号char型,则[1000 0000]补 即 为[1000 0000]原 = 128。


Signedinteger