闲扯原码,补码和反码
始发于goal00001111的专栏;允许自由转载,但必须注明作者和出处
人类习惯使用十进制数进行数值计算,而计算机则采用二进制,所以为了让计算机帮助人类计算,首先要把十进制数转换为二进制数。本文以最简单的8位定点整数为例,分析了计算机存储和计算数值的方法。
地球人都知道,整数有正负之分,但计算机却只认得“0”“1”,不知道符号“+”和“-”,所以有必要用“0”“1”来表示“+”“-”。人们规定用“0”表示“+”,用“1”表示“-”。
这样,我们就可以表示出计算机能识别的整数了,我们把符号数值化后的二进制数称为机器数,相对应的,符号没有数值化(即仍用“+”“-”号表示)的二进制数称为真值。计算机只能处理机器数,不认识真值,真值是给人类看的。
机器数有三种编码形式,分别称为:原码,补码和反码。为什么要搞得这么复杂,那些计算机科学家真的是吃饱了没事干吗?且听我慢慢道来:
其实篇头已经介绍了机器码的一种形式——原码,它的特点是有效数值部分照抄真值,符号“+”“-”分别用“0”“1”表示。
例如,十进制数+6,它的真值是+000 0110(注意:8位二进制数最高位是符号位,所以其真值只有7位),对应的原码就是0000 0110。
又如,十进制数-6,它的真值是-000 0110,对应的原码就是1000 0110。
原码表示法比较直观,它的数值部分就是该数的绝对值,而且与真值的转换十分方便。但是它的加减法运算较复杂,当两数相加时,机器要首先判断两数的符号是否相同,如果相同则两数相加,若符号不同,则两数相减。在做减法前,还要判断两数绝对值的大小,然后用大数减去小数,最后再确定差的符号,换言之,用这样一种直接的形式进行加运算时,负数的符号位不能与其数值部分一道参加运算,而必须利用单独的线路确定和的符号位。要实现这些操作,电路就很复杂,这显然是不经济实用的。为了减少设备,解决机器内负数的符号位参加运算的问题,总是将减法运算变成加法运算,也就引进了反码和补码这两种机器数。
那如何将减法运算转化为加法运算呢?
首先引入 “模”的概念,“模”是指一个计量系统的计数范围。以我们每天用来算时间的时钟为例,时钟的计量范围是0~11,所以它的模就等于12。计算机也可以看成一个计量机器,它也有一个计量范围,即存在一个“模”。 机器字长为n位的计算机的计量范围是0~2^n-1,模=2^n。
“模”实质上是计量器产生“溢出”的量,它的值在计量器上表示不出来,计量器上只能表示出模的余数。例如,虽然时钟的模=12,但是在时钟的指针并不能真正指向“12点”,“12点”的位置和“0点”是重合的!用C语言表示就是12%12 == 0。
任何有模的计量器,均可化减法为加法运算。这是为什么呢?
仍然以时钟为例,假设当前时针指向10点,而准确时间是6点,调整时间可有以下两种拨法:
一种是倒拨4小时,即:10-4=6
另一种是顺拨8小时:10+8=12+6=6
在以12为模的系统中,加8和减4效果是一样的,因此凡是减4运算,都可以用加8来代替。
对“模”12而言,8和4互为补数。插一句,所谓“补数”,实际上是模拟了数学中“补角”的概念,如果两个角的度数之和为180度,我们就称这两个角互为补角。同样的,如果在某个计量系统中,两个数之和刚好等于模,则它们互为“补数”,例如,在以12为模的系统中,11和1,10和2,9和3,7和5,6和6都互为补数。
对于计算机,其概念和方法完全一样。机器字长为n位的计算机,设n=8, 所能表示的最大数是11111111,若再加1称为100000000(9位),但因只有8位,最高位1自然丢失,又回了00000000,所以8位二进制系统的模为2^8。 在这样的系统中减法问题可以化成加法问题,只需把减数用相应的补数表示就行了。
把补数用到计算机对数据的处理上,就是补码。补码也是一种机器码,它克服了原码的一些缺陷,一方面使符号位能与有效值部分一起参加运算,从而简化运算规则;另一方面使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计。现代的计算机都是用补码的形式来存储数据和进行算术运算的。
那补码是如何编码的,即我们如何将一个整数的真值转换为一个8位补码呢?
回到最初的例子,十进制数+6。我们已经知道了它真值是+000 0110,原码是0000 0110。并且用自然语言介绍了如何实现真值和原码的转换。但是,一个众所周知的事实是:“自然语言”不如“数学语言”严谨!我们希望能够用数学表达式来表示真值和原码的关系,这就是:
设机器字长为N位,真值为X,则:
[X]原 = X, 0 <= X < 2^(n-1)
[X]原 = 2^(n-1) - X, -2^(n-1) < X <= 0
如何来理解这个公式呢?
仍以十进制数+6和-6为例:
[+6]原 = 6,把6转换为8位二进制数,就得到原码0000 0110。(本文的最后将会提供一个把十进制数转换为机器码的C++算法实现)。
[-6]原 = 2^(8-1) – (-6) = 256 + 6 = 262,,把262转换为8位二进制数,就得到原码1000 0110。即最高位本来是0,加了一个2^(8-1)后,最高位就变成1了。
同样我们给出补码的数学表达式:
[X]补 = X, 0 <= X < 2^(n-1)
[X]补 = 2^n + X, -2^(n-1) <= X < 0
和原码一样,正数的补码就等于真值,那如何理解负数的补码呢?
例如,[-6]补 = 2^8 + (-6) = 512 – 6 = 506。
且慢,这个506怎么这么熟悉!它不正是以2^8为模的6的“补数”吗?原来负数的补码就等于它的绝对值的补数啊!
把506转换为8位二进制数,就得到-6的补码1111 1010。
得到某个数的补码后,我们就可以把减法运算转化为加法运算了。
补码加法的运算法则为:[X +Y]补 = [X] 补 + [Y] 补
例1:X =+011 0011,Y=+010 1001,求[X+Y] 补
解:[X +Y]补 = [X] 补 + [Y] 补 = 0011 0011 + 0010 1001 = 0101 1100
例2:X =+011 0011,Y=-010 1001,求[X+Y] 补
解:[X +Y]补 = [X] 补 + [Y] 补 = 0011 0011 + 1101 0111 = 0000 1010 (进位溢出)
注:因为计算机中运算器的位长是固定的(本例中只有8位),上述运算中产生的最高位进位将丢掉,所以结果不是1 0000 1010,而是0000 1010。
补码减法公式:[X - Y]补 = [X] 补 - [Y] 补= [X] 补 + [-Y] 补
其中:[-Y]补称为负补,求负补的办法是:对补码的每一位(包括符合位)求反,且未位加1。
例3:X =+011 0011,Y=+010 1001,求[X-Y] 补
解:[X - Y]补 = [X] 补 + [-Y] 补 = 0011 0011 + 1101 0111 = 0000 1010 (进位溢出)
例2:X =+011 0011,Y=-010 1001,求[X-Y] 补
解:[X - Y]补 = [X] 补 + [-Y] 补 = 0011 0011 + 0010 1001 = 0101 1100
根据补码加减运算得到的结果仍然是补码,若要将补码转换成原码,只要对其再求一次补码就行了。
再来说说反码。当初引入反码是为了解决原码运算所遇到的困难,但由于反码自身也存在一定的缺陷,加之补码在机器运算中的优越表现,完全掩盖了反码的光芒,以至于现在人们之所以提到反码,只是因为在用笔算将真值转换为补码的时候,可以快一些——先将原码转换为反码,然后反码加1,就得到了补码——但是对于计算机来说,反码这个中介完全是没有必要的。
反码和原码的关系很紧密,反码表示法规定:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。
同样我们给出反码的数学表达式:
[X]反 = X, 0 <= X < 2^(n-1)
[X]反 = 2^n – 1 + X, -2^(n-1) < X <= 0
从数学表达式中我们可以发现,整数的三种机器码都是相同的,而负数的则不同。负数的反码是对原码按位求反(符合位除外),而补码则等于反码加1。这样有了反码这个中介,我们即使是用笔算也能够很快地将原码转换为补码了。例如:
[+6]反 = 6, [-6]反 = 2^(8-1) – 1 + (-6) = 255 + 6 = 261,,把261转换为8位二进制数,就得到反码1111 1001。
由于-6的原码为1000 0110,我们稍作观察,就可找到反码和原码的关系。
再将反码加1,就得到-6的补码1111 1010。
现在明白了吧?
附录:把十进制数转换为机器码的C++程序代码
#include <iostream>
using namespace std;
const int MAX = 32;
void Binary(char b[], int x); //将x转换为二进制数
void TrueForm(char b[], int x); //获取原码
void RadixMinus(char b[], int x); //获取反码
void Complement(char b[], int x); //获取补码
void TruthValue(char b[], int x);//获取真值
int main()
{
int x = 1;
char b[MAX+1]={0};
cout << "十进制数:" << x << endl;
TruthValue(b, x);//获取真值
cout << "真值:" << b << endl;
TrueForm(b, x); //获取原码
cout << "原码:" << b << endl;
RadixMinus(b, x);//获取反码
cout << "反码:" << b << endl;
Complement(b, x);//获取补码
cout << "补码:" << b << endl;
cout << "十进制数:" << -x << endl;
TruthValue(b, -x);//获取真值
cout << "真值:" << b << endl;
TrueForm(b, -x); //获取原码
cout << "原码:" << b << endl;
RadixMinus(b, -x);//获取反码
cout << "反码:" << b << endl;
Complement(b, -x);//获取补码
cout << "补码:" << b << endl;
system("pause");
return 0;
}
void Binary(char b[], int x)//将x转换为二进制数
{
for (int i=MAX-1; i>=0; i--)
{
b[i] = (x & 1) + '0';
x >>= 1;
}
b[MAX] = '\0';
}
void TrueForm(char b[], int x) //获取原码:根据数学表达式求得
{
if (x >= 0)
Binary(b, x);
else
Binary(b, (1<<(MAX-1)) - x);
}
void RadixMinus(char b[], int x) //获取反码:正数的反码=补码;负数的反码=补码-1
{
if (x >= 0)
Binary(b, x);
else
Binary(b, x - 1);
}
void Complement(char b[], int x) //获取补:数据在计算机中以补码形式存储,直接转换即可
{
Binary(b, x);
}
void TruthValue(char b[], int x)//获取真值:根据原码获得真值
{
TrueForm(b, x);
b[0] = (b[0] == '0') ? '+' : '-';
}
参考文献:
(1)Boater的博客:《反码和补码技术是怎样被提出的?》
http://blog.tianya.cn/blogger/post_show.asp?BlogID=227218&PostID=7046448
(2)北半球的孤独发帖:《关于机器数的几点注记》
http://forum.noi.cn/thread-29319-1-1.html
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/goal00001111/archive/2010/04/15/5490612.aspx