原码、反码、补码详解

2019-04-13 16:47发布

 转载自:二进制原码、反码、补码

1.基础概念和计算方法

在探求为何机器要使用补码之前, 让我们先了解原码, 反码和补码的概念.对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式. 1.1模的概念 把一个计量单位称之为模或模数
补码的模为10000 0000(2^8)
反码的模为 1000 0000(2^7,从反码的定义也能够知道,即反码的运算不涉及符号位)
模的解释
在日常生活中,有许多化减为加的例子。例如,时钟是逢12进位,12点也可看作0点。
当将时针从10点调整到5点时有以下两种方法:
(1)时针逆时针方向拨5格,相当于做减法: 10-5=5
(2)时针顺时针方向拨7格,相当于做加法:10+(12-5)=12+5=5 (模为 12) 同理,计算机的运算部件与寄存器都有一定字长的限制(假设字长为8),因此它的运算也是一种模运算。当计数器计满8位也就是256个数后会产生溢出,又从头开始计数。产生溢出的量就是计数器的模(也即256个数轮回一次,256就是模数),显然,8位二进制数,它的模数为2^8=256。在计算中,两个互补的数称为“补码”。 1.2原码 原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制: [+1]原 = 0000 0001 [-1]原 = 1000 0001 第一位是符号位. 因为第一位是符号位, 所以8位二进制数的取值范围就是: [1111 1111 , 0111 1111] 即 [-127 , 127] 原码是人脑最容易理解和计算的表示方式 1.3反码 反码的表示方法是: 正数的反码是其本身 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反. [+1] = [00000001]原 = [00000001]反 [-1] = [10000001]原 = [11111110]反 可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算 1.4补码 补码的表示方法是: 正数的补码就是其本身 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1) [+1] = [00000001]原 = [00000001]反 = [00000001]补 [-1] = [10000001]原 = [11111110]反 = [11111111]补 对于负数, 补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值. 1.5其他概念
由于计算机中符号和数字一样,都必须用二进制数串来表示,因此,正负号也必须用0、1来表示。
用最高位0表示正、1表示负, 这种正负号数字化的机内表示形式就称为“机器数”,
而相应的机器外部用正、负号(即”+”、”-“)表示的数称为“真值”,将一个真值表示成二进制字串的机器数的过程就称为编码。

2.为何要使用原码、反码和补码

现在我们知道了计算机可以有三种编码方式表示一个数. 对于正数因为三种编码方式的结果都相同: [+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=0 1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0 发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在”0”这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0. 于是补码的出现, 解决了0的符号以及两个编码的问题: 1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原 这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128: (-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补 -1-127的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是注意因为-0的补码与+0相同均为[0000 0000]补 所以补码能比原码和反码多表示一个负数即-128, 所以-128并没有原码和反码表示.(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的) 使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127]. 因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-2^31, 2^31-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值. 而且实际上并不是从10000001到11111111依次表示-1到-127,而是刚好相反的,从10000001到11111111依次表示-127到-1
即0000 0000~0111 1111表示+-0~+127
而1000 0000~1111 1111表示-128~-1 用补码表示负数时:负数X用2^n - |X|来表示,其中n为机器的字长 当n=8时,[-1]补 = 2^8 - 1 = 11111111, [-127]补 = 2^8 - 127 = 100000001 [-0]补=2^8=00000000在补码表示法中只有一种表示,即00000000

3.原码、补码和反码的进一步说明

在计算机里如何表示整数?
整数有无穷多个,在计算机里,通常我们只能表示出其中的一部分。假如我们用 n 个比特来表示一个整数。1 个比特有 2 个状态,n 个比特就有 2^n 个状态,把这 2^n 个状态的集合记为 A. 显然,用 A,我们可以与 n 个整数建立起一一对应。我们还希望 A 所表示的整数能够像整数那样地运算—整数,像整数那样运算,这是不是一句废话?数学中的整数相加,仍然是一个整数,但 A 里两个整数相加,我们却无法保证它们的和仍在 A 中,用代数的术语来讲,叫做 “不满足封闭性”,这是个很坏的性质。
3.1补码
不过数学上有处理这个问题的成熟方案,如果我们能后退一步,让 A 表示的是模 |A| 的剩余类,则加法运算马上就封闭了。而且这个时候 A 不仅可以与 2^n 个整数对应起来,而且,在某种意义下,可以与整数环 Z 对应起来。用代数的观点,这个 “某种意义”就是所谓的同态。
整数有两种封闭运算,一种是加法,另一种是乘法。A 作为模 2^n 的剩余类,也有加乘两种运算。定义 Z 到 A 的映射
f(x) = m mod 2^n
f 是一个同态,也就是说,f 满足这样的良好性质:
f(x+y) = f(x) + f(y)
f(xy) = f(x)f(y)
我们通常使用 10 进制数,在这个进制下,f(x) 并不容易计算,但是在计算机里,本质的表示是二进制,于是 f(x) 的运算变得出奇地简单。如果 x 小于 2^n,则 x 的 2 进制表示就是 f(x),如果 x>=2^n,则要求其模 2^n 的余数,这恰好是 x 二进制表示的最低 n 位,换句话说,简单地把高位抛弃就行了。顺便指出,f(0)=0, f(1)=1.
我们来看一看 A 中的加法,f(x)+f(y), 若结果小于 2^n,则运算自然封闭,如果 f(x)+f(y) >= 2^n,则取其最低的 n 位,用电路实现时,可以简单地扔掉高位,保留低位。
到目前为止,一切都很好,但是减法怎么办呢?对整数运算而言,减去 a 不过是加上 a 的相反数的同义语。只要对 A 中的每个元素,能容易计算出其相反数就可以了。理论上 f(x) 的相反数就是
f(-f(x))
不过这个好像不容易计算,因为我们现在并没有给出 A 中”负数”的概念,事实上 A 是模 2^n 的剩余类环,根本就没有所谓的负数。
这个困难也是容易处理的。作为 f(x) 的相反数,-f(x) 应该满足这样的性质:
-f(x) + f(x) = f(0) = 0
所以我们只要有在 A 中找一个元素,使得它与 f(x) 的和是 0 就可以了。但是 f(x) 本身可能含有很多比特 1,加上一个数能使它们变成 0 吗? 考虑到 A 中的加法要模去一个 2^n,这个问题实际上很好办,只要让求出的和是 2^n 就可以了。所以难发现 -f(x) 就是把 f(x) 的比特”取反”—即 0 变 1, 1 变 0,并加上 1 就得到了 -f(x)。容易验证:
f(-x) = - f(x).
现在我们回过头来看前面的一句话,红 {MOD}部分:
“我们通常使用 10 进制数,在这个进制下,f(x) 并不容易计算,但是在计算机里,本质的表示是二进制,于是 f(x) 的运算变得出奇地简单。如果 x 小于 2^n,则 x 的 2 进制表示就是 f(x),如果 x>=2^n,则要求其模 2^n 的余数,这恰好是 x 二进制表示的最低 n 位,换句话说,简单地把高位抛弃就行了。顺便指出,f(0)=0, f(1)=1.”
红 {MOD}那句话简直是胡扯,因为没有考虑到负数的情况。但 f(x) 容易计算这句话并没有错,因为当 x 为负数时,我们可以利用 f(x) = -f(-x)。 由于 -x 是正的,f(-x) 容易计算,之后 -f(x) 也不过是取反加 1 而已。
好,到目前为止—在数学世界里,简直是完美的。但是回到现实, A 里头真的没有负数,怎么办?整数能比较大小,A 里头的数又怎么办?
这时候,我们可以用一个不太完美的方案,把 A 里头的元素再映射回整数 Z 中,如果只需要无符号的数,则变换为
g(u) = u, 0<= u <2^n-1
其实就是不把 A 中的元素模 2^n 的剩余类,而直接看成整数。不过这时的运算就要考虑溢出了。如果是无符号的情形,运算的结果仍是模 2^n 的余数,外加一个溢出标志。
如果要考虑负数,如果没有特别的理由,则要求正负个数大致相等是自然的。可以考虑让 0 代表 0, 1~2^{n-1}-1 代表正数,2^{n-1}+1~2^n-1 代表负数。丢掉一个 2^{n-1} 是因为希望正数和负数刚好配对,若 u 代表负数,则其落在 2^{n-1}+1~2^n-1 中,但它代表负几呢?当然是负 -u 了。从两个相反数之和为 0 ,或 2^n 的观点,-u 就是 2^n-u, 而 u 则是 -(-u) 即 u-2^n。
2^{n-1} 是个麻烦,在 A 中,它的相反数就是自己,把它当正数,负数都不大合理。出于完整性考虑,随便选一个好了。在这里我们让 2^{n-1} 为负数,理由是它的最高位是 1,跟其它”负数”长得比较象。
换个角度看,模 n 的剩余类,或者余数为 0,1,…,2^{n-1}, 2^{n-1}+1, …, 2^n-1. 这些余数也可以表示为:
0,1~2^{n-1}, 2^n-(2^{n-1}-1), …, 2^{n}-1
我们刚才所做的就是把上面数列中 2^{n} -r (1<= r<= 2^{n-1}-1)类型的的数对应为负数 -r 而已。用代数的术语,就是在陪集中选取了不同的代表元。
于是我们定义 A 到 Z 的变换,使得
当 0<= u <= 2^{n-1} 时, g(u) = u
当 2^{n-1}

4.补码的原理及随想

越早在课堂上学的东西,越给我以简单的印象,忘得也越快。而事实上,它们往往是最富有智慧的,即便在我没忘的时候,也没有深刻地理解它们。
嘛是补码?不少书上扯一堆“取反加1”之类的规则,很不着重点,我觉得核心在于:
对于范围为[0,M)的整数计量系统,其模为M。和为M的两个数互为补数。
如果有两个整数a,b∈[0, M),那么f(a-b)==f(a+c),其中c= M-b,是b的补码,
f是一个映射,定义为:
当0 <=x < M时,f(x)=x;
当x>= M时,f(x)=x % M;——–>此时就要消除溢出
当x <0时,f(x)=f(M +x).
其中%为取余运算(效果同编程语言中的取模运算)。
在计算机中,f是由溢出隐式实现的,所以天生就有a-b==a+c。这就把减运算转化成了加运算。
于是,为了便于执行减运算,计算机就把-b表示为其补码c。
假设机器字有n位,那么M=2n,c=2n-b。
人在纸上怎么计算2n-b的二进制值?2n的原码就是1后面跟了n个0,直接用它减b的原码不方便,先用2n-1的原码(n个1)减b的原码,得到的结果加上1就是2n-b的值了——这就是“取反加1”的由来。

5.有符号数,无符号数

同样是年纪和工资,前者不需要有负值,但后者可能需要——至少所有的老板都这样认为。那么,负数在计算机中如何表示呢?这一点,你可能听过两种不同的回答。 一种是教科书,它会告诉你:计算机用“补码”表示负数。可是有关“补码”的概念一说就得一节课,这一些我们需要在第6章中用一章的篇幅讲2进制的一切。再者,用“补码”表示负数,其实一种公式,公式的作用在于告诉你,想得问题的答案,应该如何计算。却并没有告诉你为什么用这个公式就可以和答案? 另一种是一些程序员告诉你的:用二进制数的最高位表示符号,最高位是0,表示正数,最高位是1,表示负数。这种说法本身没错,可是如果没有下文,那么它就是错的。至少它不能解释,为什么字符类型的-1用二进制表示是“1111 1111”(16进制为FF);而不是我们更能理解的“1000 0001”。(为什么说后者更好理解呢?因为既然说最高位是1时表示负数,那1000 0001不是正好是-1吗?)。 让我们从头说起。 5.1你自已决定是否需要有正负 就像我们必须决定某个量使用整数还是实数,使用多大的范围数一样,我们必须自已决定某个量是否需要正负。如果这个量不会有负值,那么我们可以定它为带正负的类型。 在计算机中,可以区分正负的类型,称为有符类型,无正负的类型(只有正值),称为无符类型。 数值类型分为整型或实型,其中整型又分为无符类型或有符类型,而实型则只有符类型。 字符类型也分为有符和无符类型。 比如有两个量,年龄和库存,我们可以定前者为无符的字符类型,后者定为有符的整数类型。 5.2使用二制数中的最高位表示正负 首先得知道最高位是哪一位?1个字节的类型,如字符类型,最高位是第7位,2个字节的数,最高位是第15位,4个字节的数,最高位是第31位。不同长度的数值类型,其最高位也就不同,但总是最左边的那位(如下示意)。字符类型固定是1个字节,所以最高位总是第7位。 (红 {MOD}为最高位) 单字节数: 1111 1111 双字节数: 1111 1111 1111 1111 四字节数: 1111 1111 1111 1111 1111 1111 1111 1111 当我们指定一个数量是无符号类型时,那么其最高位的1或0,和其它位一样,用来表示该数的大小。 当我们指定一个数量是无符号类型时,此时,最高数称为“符号位”。为1时,表示该数为负值,为0时表示为正值。 5.3无符号数和有符号数的范围区别 无符号数中,所有的位都用于直接表示该值的大小。有符号数中最高位用于表示正负,所以,当为正值时,该数的最大值就会变小。我们举一个字节的数值对比: 无符号数: 1111 1111 值:255 1* 27 + 1* 26 + 1* 25 + 1* 24 + 1* 23 + 1* 22 + 1* 21 + 1* 20 有符号数: 0111 1111 值:127 1* 26 + 1* 25 + 1* 24 + 1* 23 + 1* 22 + 1* 21 + 1* 20 同样是一个字节,无符号数的最大值是255,而有符号数的最大值是127。原因是有符号数中的最高位被挪去表示符号了。并且,我们知道,最高位的权值也是最高的(对于1字节数来说是2的7次方=128),所以仅仅少于一位,最大值一下子减半。 不过,有符号数的长处是它可以表示负数。因此,虽然它的在最大值缩水了,却在负值的方向出现了伸展。我们仍一个字节的数值对比: 无符号数: 0 —————– 255 有符号数: -128 ——— 0 ———- 127 同样是一个字节,无符号的最小值是 0 ,而有符号数的最小值是-128。所以二者能表达的不同的数值的个数都一样是256个。只不过前者表达的是0到255这256个数,后者表达的是-128到+127这256个数。 一个有符号的数据类型的最小值是如何计算出来的呢? 有符号的数据类型的最大值的计算方法完全和无符号一样,只不过它少了一个最高位(见第3点)。但在负值范围内,数值的计算方法不能直接使用1* 26 + 1* 25 的公式进行转换。在计算机中,负数除为最高位为1以外,还采用补码形式进行表达。所以在计算其值前,需要对补码进行还原。这些内容我们将在第六章中的二进制知识中统一学习。 这里,先直观地看一眼补码的形式: 以我们原有的数学经验,在10进制中:1 表示正1,而加上负号:-1 表示和1相对的负值。 那么,我们会很容易认为在2进制中(1个字节): 0000 0001 表示正1,则高位为1后:1000 0001应该表示-1。 然而,事实上计算机中的规定有些相反,请看下表: 二进制值(1字节) 十进制值 1000 0000 -128 1000 0001 -127 1000 0010 -126 1000 0011 -125 … … 1111 1110 -2 1111 1111 -1 首先我们看到,从-1到-128,其二进制的最高位都是1(表中标为红 {MOD}),正如我们前面的学。 然后我们有些奇怪地发现,1000 0000 并没有拿来表示 -0;而1000 0001也不是拿来直观地表示-1。事实上,-1 用1111 1111来表示。 怎么理解这个问题呢?先得问一句是-1大还是-128大? 当然是 -1 大。-1是最大的负整数。以此对应,计算机中无论是字符类型,或者是整数类型,也无论这个整数是几个字节。它都用全1来表示 -1。比如一个字节的数值中:1111 1111表示-1,那么,1111 1111 - 1 是什么呢?和现实中的计算结果完全一致。1111 1111 - 1 = 1111 1110,而1111 1110就是-2。这样一直减下去,当减到只剩最高位用于表示符号的1以外,其它低位全为0时,就是最小的负值了,在一字节中,最小的负值是1000 0000,也就是-128。 我们以-1为例,来看看不同字节数的整数中,如何表达-1这个数: 字节数 二进制值 十进制值 单字节数 1111 1111 -1 双字节数 1111 1111 1111 1111 -1 四字节数 1111 1111 1111 1111 1111 1111 1111 1111 -1 可能有同学这时会混了:为什么 1111 1111 有时表示255,有时又表示-1?所以我再强调一下本节前面所说的第2点:你自已决定一个数是有符号还是无符号的。写程序时,指定一个量是有符号的,那么当这个量的二进制各位上都是1时,它表示的数就是-1;相反,如果事选声明这个量是无符号的,此时它表示的就是该量允许的最大值,对于一个字节的数来说,最大值就是255。

6.参考文献

http://hi.baidu.com/plato/item/45151446c831c017886d1033 http://www.cnblogs.com/ASPNET2008/archive/2009/04/29/1446471.html http://blog.csdn.net/macky0668/article/details/3917878 http://www.cnblogs.com/jason-xiao/archive/2009/05/05/1450313.html http://blog.csdn.net/cereslei/article/details/6713865 http://blog.sina.com.cn/s/blog_56d8ea900100y6nl.html