补码疑云--转载

原文地址:http://blog.csdn.net/qq_35263706/article/details/69523384


补码疑云

从大一到现在了,学的东西越来越多对计算机也越来越了解,然而补码就像一个阴魂不散的家伙,从大一到大二一直都困扰着我。每次都是一百度就感觉自己好像懂了似的,然而过得几天就又搞混了,我就发誓一定要征服它,然而每次都是无功而返,直到最近学汇编,突然间茅塞顿开,以前看的东西“噌”的一下就全都想起来了。为了让大家能更深刻的认识补码,而不至于像我一样似懂非懂经常感到困惑于是就写下了这篇文章,也算为自己留个纪念吧。好了废话少说进入正题:(再啰嗦一句:此文只是通俗的说补码,并不涉及严谨的推理和证明)
总论:1》用补码,减法变加法
2》无符号直接存,有符号存补码,cpu不区分有符号无符号,统统当作无符号,是否有符号取决于程序员对该结果的解释
(有点绕啊,没事)

第一个部分:补码怎么算
1》补码,是针对有符号数来说的
定义n位有符号变量 a=a1,a2,a3…..an
补码的作用: 减去一个数=加上他的补码
补码的计算公式:a= -1*a1*(2^n)+a2*[2^(n-1)]+a3*[2^(n-2]+….+an*(2^0)(记住此公式非常重要以后求补码直接算就行了)
这个公式的意思是:最高位的权值为负,其余位的权值全为正。
举个例子吧: 定义四位有符号变量 a=1110
最高位权值为-1,所以以-8为基调,低三位 为110,表示正数6,所以-8+6=-2,于是乎-2的补码就是1110
再举个例子:定义四位有符号变量 b=0001,c=0000,d=1000
b最高位为0,所以以0为基调,低三位001表示正数1,所以0+1=1,于是乎正数1的补码就是它本身0001
c最高位为0,所以以0为基调,低三位000表示0,所以0+0=0,于是乎0的补码就是它本身
而无符号数则不同,没有符号就没有正负,没有正负就不用补码表示,直接存
2》补码运算怎么判断溢出
正数+正数和负数+负数才会发生溢出(正数-负数等价于正数+正数,负数-正数等价于负数+负数)。如何判断溢出呢?根据上面的补码计算公式来:
四位有符号数 a=1100(-4),b=1011(-5),用a_0表示最高位(符号位),a_1表示第二位,以此类推……
方法:最高位上的进位和第二高位上的进位进行异或,值为1表示有溢出。
什么意思呢?我们可以把最高位和低三位分开来算。
(最高位+最高位)+(低3位+低三位)。
a_0+b_0=-8+-8=-16(有进位)而(a1a2a3+b1b2b3)=100+011=7没有进位
于是就发生了溢出。我们可以这样理解:最低可以表示-8,如果你最高位有两个-8如果低三位相加不足以产生一个进位8来抵消掉一个-8,那么-8加上任意一个负数都会溢出,如果低三位相加产生了一个进位,那么就可以抵消掉一个-8,还剩一个-8,-8加上任意一个正数都不会溢出,此时最高位进位位有进位,第二高位也有进位,相异或得0,无溢出
同理可得:正数+正数(0100+0101=4+5=9,溢出)

第二个部分:编译器怎么存数
我们在编写程序时经常会区分有符号数和无符号数,我们在看相关书籍时也经常会遇到有符号数转无符号数,无符号数转有符号数,每次看到这我就不想看,因为看一次忘一次,忘一次又看一次,如此反复,快抓狂了。。。直到学了汇编,以前看的噌的一下就理解了。。。下面细细道来。

有符号无符号数相互转换

//先讲有符号
//正数:Positive 负数:Negative
#include <iostream>
using namespace std;
int main()
{
    char P_1=1;
    char N_1=-1;
    char P_max=127;
    char N_max=-128;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我们以char为例子,我们把在debug模式下看一看上述代码的汇编码,看编译器到底存的是什么(codeblocks16.01+mingw)
这里写图片描述

 最高位表示符号位 
有符号数    源码(二进制) 编译器存的数(二进制)     怎么来的
P_1       0000_0001    0000_0001(0x1)    补码公式计算可得
N_1       1000_0001    1111_1111(0xff)   补码公式计算可得
zero      0000_0000    0000_0000(0x0)    补码公式计算可得
P_127     0111_1111    0111_1111(0x7f)   补码公式计算可得
N_128  溢出,源码无法表示  1000_0000(0x80)   补码公式计算可得
(这也是补码比源码好的一个方面,可以多表示一个数-128)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从上面我们可以看出:编译器对于有符号数并不是存源码,而是直接存的补码,从源码到补码的计算是在编译的时候由编译器计算的。
编译器存的是补码也就是说在这个内存单元里存放的数是补码。当你把它当成有符号数进行计算时,会将补码解释成为有符号数,但如果你把它看成无符号数,则会将补码解释成无符号数(这句话下面还会解释)。举个例子吧 N_1 ,存的是0xff,解释成有符号数就是 -1 ,解释称无符号数就是255。下面用一张图来说明将有符号数转成无符号数后结果是多少
这里写图片描述

//无符号数则直接存的是源码
//看字不重要,看图
这里写图片描述

//综:有符号无符号数之间的相互转换
//看字不重要,看图
这里写图片描述

小技巧:如果你想看一下一个有符号数在计算机是怎么存放的,又不懂汇编语言,没事,其实只要把它转换成无符号数然后再cout就可以证明你的理解是否正确,动手试一试吧
举个例子:

       unsigned int a=0xffffffff
       cout<<(signed int)a<<endl
       你猜会在屏幕上输出几
       signed     int  b=-1
       cout<<(unsigned int)b<<endl;
       你再猜会在屏幕上输出几









        不出意外:-14294967295
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(附:如果是char,因为char是字符类型,故转换成unsigned int类型才可以打出数字,因为转换成unsigned int类型是0扩展,最高位补0,对数值大小无影响)

第三部分:硬件部分——cpu加法器

计算机中只有加法没有减法,为什么呢?我们又为什么要采用补码呢?原因就是:减去一个数等于加上这个数的补码。如果要求a-b,那么只需把b转换成补码 b补,然后执行a+(b补)就可以了,一个加法器便可同时实现加法减法。如果为加法和减法设计专门的加法器和减法器,这样就增加了成本,若采用补码,则只用一个加法器便可同时实现加法和减法,何乐而不为?cpu就是这么干的
下面讲一讲加法器的构造:
//看字不重要,看图
这里写图片描述

上图就是CPU加法器的一个超简单的实现(图片出处:《逻辑与计算机设计基础》第四版 P298 图9-3)
上面的算术运算模块有两个操作数 A和B以及进位C0,通过一个两位选择信号S1,S0来选择执行加法或减法,注意:B操作数并不是直接送到加法器,而是通过一个B输入模块间接送到加法器的。
cpu实际是执行A+B+C0
用~B表示B的反码
当S1S0=01时选中减法,此时B输入逻辑模块就会将B操作数取反,同时也会将C0置1,此时cpu执行的是 A+(~B)+1仔细观察上式就会发现上面执行的其实就是A+(B的补码)等价于 A-B(求补码:取反加一)
当S1S0=00时选中加法,此时B输入逻辑模块就什么也不做,让加数B直接通过该模块,同时也会将C0置0,此时cpu执行的是 A+B+0 等价于 A+B
说了这么多还是没说清楚我们在代码中书写的加法和减法是怎么与cpu的加法和减法对应起来的呢,没事,继续往下看:
前面总论里说过:cpu才不区分什么有符号无符号数呢,它只知道把他们全部当成无符号数,一通乱算,至于结果是否有符号则由程序员决定即对结果的解释由程序员决定。
//还是原来那张图
这里写图片描述

从中我们可以看到cpu是从内存单元取数据而不是从你的代码中取数据,也就是说cpu并不知道你为什么要在这个内存单元里存11111111和10000000,也不知道他们代表的数字是多少,是有符号还是无符号,cpu说了:你在这个单元里存的数据我会把他们通通当作无符号数来进行运算,也就是说一旦进入到cpu计算阶段就没有什么补码源码了,通通都是无符号数,至于你要存什么,为什么要这么存那就是你的事了,我不管,我只管算。什么意思呢?我们可以这样理解:cpu是一个能对两个数进行运算的黑匣子,你给他两个数,然后他给你一个结果,因为它用了特殊的运算方法(把所有的操作数都当做无符号数来计算,并且补码+补码得出的结果是实际结果的补码),所以为了能得到正确的结果,就需要在编译时把算式中的操作数转变成适当的形式(有符号数存补码无符号数直接存),然后运用cpu特殊的计算方法,得到一个特殊的结果,因为这一个特殊的结果不一定是我们所希望的,所以我们就需要选择一种合适的方法来将cpu给出的结果转换成我们所需要的结果(有符号数则将结果解释成补码,无符号数则按无符号数公式计算)
还是放图吧:
这里写图片描述

继续往下看:
假设代码中有如下代码 :c=a+b
如果你是这样定义的 :signed int a,b,c;
那么编译器就会在内存单元里存a的补码A,b的补码B,然后cpu就会将A和B直接相加得到结果R,结果R也是补码(别告诉我补码+补码!=补码啊),因为你定义的c也是有符号数,所以呢你在屏幕上输出c的时候呢你的程序就会将R解释成有符号数,然后按照补码的计算公式来得出c的值,然后输出。
假设代码中有如下代码 :c=a+b
如果你是这样定义的 :unsigned int a,b,d;
那么编译器就会在内存单元里存a的源码a,b的源码b,然后两个数直接相加就得到了R。如果你把R解释成无符号数,那么只要按照无符号数的计算公式来计算就能得到正确的得到无符号数d了吧
如果你要得到有符号数c那么就又回到了上面说过的有符号无符号数之间的转换那个问题了
如果你问:如果一个有符号数和一个无符号数相加会怎么样呢?
我想说: 难道你看的那些编程书籍里没说过这通常是可能导致BUG的地方吗,这一切都是你这个程序员的责任啊.编译器通常都会把两个加数隐式转换成有符号数或者无符号数.之后两个操作数要么都有符号,要么都没有符号,就又回到了前面说的两个有符号数相加,两个无符号数相加
光说你可能不理解,那我举个例子吧


#include<iostream>
using namespace std;
int tmp=0;
int main()
{
    char*p0="c对应的内存单元里的内容(十进制): c=";
    char*p1="将a+b的结果对应的内存单元里的值解释成有符号数c:";
    char*p2="将a+b的结果对应的内存单元里的值解释成无符号数d:";
    //实际存放的是:     1111_1111_1111_1111_1111_1111_1111_1110(A)
     //实际存放的是:  + 0000_0000_0000_0000_0000_0000_0000_0001(B)
     // ---------------------------------------------------------
//实际得到的结果是       1111_1111_1111_1111_1111_1111_1111_1111(R)
//内存单元存放的就是     1111_1111_1111_1111_1111_1111_1111_1111(R)
    signed int c;
    unsigned int d;
    signed int a=-2;
    signed int b=1;
    c=a+b;    
    d=a+b;        
    cout<<p1<<c<<endl;
    cout<<p2<<d<<endl;
   // 注:最大的无符号int 2^32-1=4294967295
}
//运行结果如下:
//将a+b的结果对应的内存单元里的值解释成有符号数c:-1
//将a+b的结果对应的内存单元里的值解释成无符号数d:4294967295
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

看到这你是否有点理解为什么说cpu不区分有符号与无符号数了,得到的运算结果是否有符号取决于程序员对该值的解释了吧。

上面说了加法,忘了说减法了:
两个数相减 (- A)-(-B)
在编译时-A和-B就会转换成相应的形式(存补码),此时操作数已经不存在正负了即cpu在计算时就是C-D的形式了,cpu在计算的时候看到是减法,就会对送到算术运算模块的那个减数(D)再进行一次求补,即:在程序编译的过程中对操作数进行的求补码操作和cpu在计算减法的时候对操作数求补码的操作是两个不同的过程。

第四部分:汇编指令部分
有些汇编指令是通用的不区分有符号数无符号数如:
//前面说了,加法减法因为使用了补码,所以加法减法指令是通用的
//只有一套
add A,B //此指令就是进行A+B然后把结果存放到A里
sub A,B //此指令就是进行A-B然后把结果存到A里

然而我可没说补码对于有符号数和无符号数的乘除法运算可以共用一套指令啊,故乘除法指令有两套:
有符号乘法:imul指令
有符号除法:idiv指令
无符号乘法: mul指令
无符号除法: div指令

不是说cpu不区分有无符号数吗?其实是编译器在编译的时候编译器会根据你的代码翻译成相应的mul/div指令或imul/idiv指令,cpu运行mul/div指令时他就能根据指令,知道操作数是无符号的,cpu运行imul/idiv指令,他就
知道操作数是有符号的
然而实际过程中这里还有点疑问:不管是有符号数乘法还是无符号数乘法,编译器编译之后的汇编代码中使用的都是imul指令……找了半天资料也没找到。。。可能要去学一下64位intel汇编才能解释。。明朗的天空下还留有一点阴影,这让我想起了高中物理书3-5中一段经典的话:
1900年新春之际,著名物理学家开尔文勋爵在送别旧世纪所作的讲演中讲道:“19世纪已将物理学大厦全部建成,今后物理学家的任务就是修饰、完美这座大厦了。”同时他也提到物理学的天空也飘浮着两朵小小的,令人不安的乌云,一朵为以太漂移实验的否定结果,另一朵为黑体辐射的紫外灾难。实际上“乌云”不止这两朵,还包括气体比热中能量均分定律的失败、光电效应实验、原子线光谱等。然而,就是这几朵乌云带来了一场震撼整个物理学界的革命风暴,导致了现代物理学的诞生。
也许这里也暗藏着某些不为人知的秘密和技巧,你是否就是那个新大陆的发现者,加油!也许下一篇博文的作者就是你了。

   结束:累死了!你看到这里也不容易啊,我码到这里更不容易啊,哈哈哈。。
   来,让我们长舒一口气 吁~~~~~~~:围绕在你头顶的那团补码疑云是否已经消散  
  • 1
  • 2
  • 3
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值