打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
原码,补码和反码

始发于goal00001111的专栏;允许自由转载,但必须注明作者和出处

 

人类习惯使用十进制数进行数值计算,而计算机则采用二进制,所以为了让计算机帮助人类计算,首先要把十进制数转换为二进制数。本文以最简单的8位定点整数为例,分析了计算机存储和计算数值的方法。

地球人都知道,整数有正负之分,但计算机却只认得“0”“1”,不知道符号“+”和“-”,所以有必要用“0”“1”来表示“+”“-”。人们规定用“0”表示“+”,用“1”表示“-”。

       这样,我们就可以表示出计算机能识别的整数了,我们把符号数值化后的二进制数称为机器数,相对应的,符号没有数值化(即仍用“+”“-”号表示)的二进制数称为真值。计算机只能处理机器数,不认识真值,真值是给人类看的。

       机器数有三种编码形式,分别称为:原码,补码和反码。为什么要搞得这么复杂,那些计算机科学家真的是吃饱了没事干吗?且听我慢慢道来:

       其实篇头已经介绍了机器码的一种形式——原码,它的特点是有效数值部分照抄真值,符号“+”“-”分别用“0”“1”表示。

例如,十进制数+6,它的真值是+000 0110(注意:8位二进制数最高位是符号位,所以其真值只有7位),对应的原码就是0000 0110

又如,十进制数-6,它的真值是-000 0110,对应的原码就是1000 0110

原码表示法比较直观,它的数值部分就是该数的绝对值,而且与真值的转换十分方便。但是它的加减法运算较复杂,当两数相加时,机器要首先判断两数的符号是否相同,如果相同则两数相加,若符号不同,则两数相减。在做减法前,还要判断两数绝对值的大小,然后用大数减去小数,最后再确定差的符号,换言之,用这样一种直接的形式进行加运算时,负数的符号位不能与其数值部分一道参加运算,而必须利用单独的线路确定和的符号位。要实现这些操作,电路就很复杂,这显然是不经济实用的。为了减少设备,解决机器内负数的符号位参加运算的问题,总是将减法运算变成加法运算,也就引进了反码和补码这两种机器数。

那如何将减法运算转化为加法运算呢?

首先引入 “模”的概念,是指一个计量系统的计数范围。以我们每天用来算时间的时钟为例,时钟的计量范围是011,所以它的模就等于12。计算机也可以看成一个计量机器,它也有一个计量范围,即存在一个 机器字长为n位的计算机的计量范围是02^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而言,84互为补数。插一句,所谓“补数”,实际上是模拟了数学中“补角”的概念,如果两个角的度数之和为180度,我们就称这两个角互为补角。同样的,如果在某个计量系统中,两个数之和刚好等于模,则它们互为“补数”,例如,在以12为模的系统中,111102937566都互为补数。

对于计算机,其概念和方法完全一样。机器字长为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]

1X =+011 0011Y=+010 1001,求[X+Y]

解:[X +Y] = [X] + [Y] = 0011 0011 + 0010 1001 = 0101 1100

2X =+011 0011Y=-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

3X =+011 0011Y=+010 1001,求[X-Y]

解:[X - Y] =  [X] + [-Y] = 0011 0011 + 1101 0111 = 0000 1010 (进位溢出)

2X =+011 0011Y=-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') ? '+' : '-';  

}

 

参考文献:

1Boater的博客:《反码和补码技术是怎样被提出的?》

http://blog.tianya.cn/blogger/post_show.asp?BlogID=227218&PostID=7046448

2北半球的孤独发帖:《关于机器数的几点注记》

http://forum.noi.cn/thread-29319-1-1.html



最近有人提到char和unsigned char有什么区别,当然这个问题如果刚学计算机或者编程语言的人来说,非常简单。我也这么认为,无非就是有符号和无符号的差别嘛。


这个问题让我想到了以前学习计算机常识的时候关于补码,原码,反码的差异。这里摘取参考文章【1】中的部分内容:


注意:此处的'=='是相等的意思。'='是赋值的意思。

在机器世界里:
正数的最高位是符号位0,负数的最高位是符号位1。
对于正数:反码==补码==原码。
对于负数:反码==除符号位以外的各位取反。
     补码==反码+1.
     原码==补码-1后的反码==补码的反码+1。(读完本文后,应该能够直观地认识到本式的正确性)


可以轻易发现如下规律:
自然计算 :a-b==c.
计算机计算:a-b==a+b的补码==d.
c的补码是d.
通过此法,可以把减法运算转换为加法运算。


所以补码的设计目的是:
1.使符号位能与有效值部分一起参加运算,从而简化运算规则.
2.减运算转换为加运算,进一步简化计算机中运算器的线路设计.


讲的非常清晰了吧,是的。但是在计算机中,常做类型转换,当char或者unsigned char转换成int的时候,两者的差异是显而易见的。这里采用了部分文章【2】的代码对转换过程做了验证。


1)当我对uch和sch同时赋值-100的时候uch和sch都是十六进制的0x9c

2)此时由于两者一个是有符号,另一个是无符号的,我们可以看到十进制输出的时候,无符号的是156,而有符号的,最前面一个bit解释为了负值 -100

3)然后我们看下对uch进行类型转换(int)然后看下真值,原码,反码和补码

4)最后我们看下对sch进行类型转换(int)然后看下真值,原码,反码和补码


可以看出uch和sch最大的差异就是前面的那个符号位,仅仅那一个bit位,对于我们计算机来说,存储的内容(补码)将是绝然不同的。



真值,原码,反码和补码转换代码请详见参考文章【2】

/* 检查uchar */

void CheckUchar(unsigned char uch)

{

    int x;

    char b[MAX+1]; 

x = uch;

    printf("CheckUchar Decimal value:%d\n", x);


    TruthValue(b, x);//获取真值

    printf("TruthValue:\t%s\n", b);


    TrueForm(b, x); //获取原码

    printf("TrueForm:\t%s\n", b);


    RadixMinus(b, x);//获取反码 

    printf("RadixMinus:\t%s\n", b);


    Complement(b, x);//获取补码

    printf("Complement:\t%s\n", b);

}


/* 检查schar */

void CheckSchar(char sch)

{

    int x;

    char b[MAX+1]; 


x = sch;

    printf("CheckSchar Decimal value:%d\n", x);


    TruthValue(b, x);//获取真值

    printf("TruthValue:\t%s\n", b);


    TrueForm(b, x); //获取原码

    printf("TrueForm:\t%s\n", b);


    RadixMinus(b, x);//获取反码 

    printf("RadixMinus:\t%s\n", b);


    Complement(b, x);//获取补码

    printf("Complement:\t%s\n", b);

}


int main()

{

    unsigned char uch = -100;

    char sch = -100;

    printf("hex: uch = 0x%x, sch = 0x%x\n", uch, sch);

    printf("dec: uch = %d, sch = %d\n", uch, sch);

CheckUchar(uch);

CheckSchar(sch);

return 0;

}

参考文章:

【1】反码和补码技术是怎样被提出的?

【2】闲扯原码,补码和反码

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
为什么char的范围是 —128~ 127
【重要】关于有符号数与无符号数的一些总结
原码、补码和反码--硬件数学处理工作室
爱看
UC头条:进阶C语言——数据的存储[详解]
15W4K58S4 实验2:格式化输出
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服