更新于 

定点数的表示与运算

定点数的表示

无符号数

无符号数就是我们通常所说的非负数,所以整个机器字长的全部二进制位都为数值位,没有符号位,所以无法表示负数,相当于数的绝对值。例如:

这里要记住一个小知识点,一个数值串后面是B表示二进制,D表示十进制,H表示十六进制。所以对于n位机器字长,无符号数可以表示的数量是2^n个数:

例如对于8位机器字长的计算机,他可以表示的无符号数范围是:

所以n位无符号数表示的范围是:0~2^n-1。

有符号数

有符号数即会最高位符号位来决定数值的正负,0是正数,1是负数。并且有符号数的机器表示会有多种形式如原码、补码、反码和移码。为了对上面四中形式进行区分,我们规定用X表示真值:

[X]表示原码,[X]表示补码,[X]表示反码,[X]表示移码[X]_原表示原码,[X]_补表示补码,[X]_反表示反码,[X]_移表示移码

并且有符号数表示的数值数量和无符号数相同都是2^n个,但是表示的范围是不同的,对于一个n位机器字长的计算机,他能表示的有符号数的范围是-(2^n-1)~2^n-1。

定点表示

那么无符号数和有符号数和定点表示有什么关系?我们知道在机器的数值表示中,为了能够表示小数,我们通常会有一个小数点位置,那么根据这个小数点位置是否固定不变,我们将数划分成了定点表示和浮点表示两种形式。而一般对于一个实数,通常使用的都是定点表示,所以使用定点表示的整数我们称之为定点整数,小数称之为定点小数。那么这里我们就详细就讲解一下定点表示。

定点表示中小数点是事先约定好位置固定不变的,不再使用’.'表示了。这里我们分别看一下定点小数和定点整数的表示方法。

定点小数

约定小数点位置在符号位之后,有效值位最高位之前,那么后面的位权分别是2^(-1),2^(-2)等。如下图:

这里我们一定要区分一个概念,机器里的小数真的就只是小数点后面的数值,例如3.14虽然是小数,但是在机器中3看为整数又称为纯整数,0.14才看为一个真正的小数又称为纯小数。所以对于一个定点小数默认为就是有符号数,所以对于一个数值有效位n位的定点小数(x1~xn)。可以表示的范围是

  • 当x0=0,x1~xn均是1时是最大值,真值就是1-2^(-n)。
  • 当x0=1,x1~xn均是1时是原码所能表示的最小值,真值是-(1-2^(-n))。

此时存储解码时默认就全部是按照小数的位权进行解码:

那么我们如何知道对于一个二进制数值串要按照定点小数进行解码呢?可以根据参量之前的声明格式得知。

定点整数

其实定点整数就是我们广泛所指的纯整数,因为纯整数没有小数部分,所以小数点就固定在有效数值位最后一位即可。解码时位权全部按照2^0,2^1,2^2赋值即可。比如:

我们发现同样是对于011进行计算真值,但是此时由于要按照定点整数计算,所以计算处的真值就是+3D了。那么定点整数同样是有符号数,所以可以表示的范围是:

原码、补码、反码、移码

原码

原码就是我们最常见的二进制数值表示法,由最高位符号位以及有效数值位组成,符号位决定数值的正负,有效数值位表示的是数值的绝对值。

对于纯小数的原码,我们规定求解原码规则如下:

[X]={x0<=x<11x=1+x1<x<=0[X]_原=\begin{cases} x&0<=x<1\\ 1-x=1+|x|&-1<x<=0 \end{cases}

对于纯整数的原码,我们规定规则如下“

[X]={0,x0<=x<2n2nx=2n+x2n<x<=0[X]_原=\begin{cases} 0,x&0<=x<2^n\\ 2^n-x=2^n+|x|&-2^n<x<=0 \end{cases}

一定要注意对于机器字长更长的,位数不全的时候要补0,但是对于小数是在有效数值位后面补0,而对于纯整数是在符号位和有效数值位中间补0。

并且在原码中,真值0有正零和负零两种形式,即[+0]原=00000,[-0]原=10000。

补码

我们发现在原码的加减计算时很复杂,尤其是在两个不同符号数的加法或者两个相同符号数的减法时,需要用绝对值大的数减去绝对值小的数,然后最后还要给结果选择合适的符号,这很麻烦,所以引入了补码。补码的加减运算全部都是采用加法运算并且结果正确。

对于纯小数的补码,我们规定规则如下:

我们发现了如下规律:

[X]>[X]{如果是正数,则不变如果是负数,符号位不变,数值位取反加一[X]_原->[X]_补\begin{cases} 如果是正数,则不变\\ 如果是负数,符号位不变,数值位取反加一 \end{cases}

[X]>[X]:连同符号位整体取反加一[X]_补->[-X]_补:连同符号位整体取反加一

比如上面的x2=-0.1001的原码是1,1001000,那么他的补码就是符号位不变,数值位取反再加一即可得1,0111000。又因为x2和x1是正负数,所以x2的补码也可以由x1的补码推得,即x1的补码是0,1001000,所以x2的补码就是x1的补码整体取反加一得1,1001000。

对于纯整数的补码,我们规定规则如下:

同样也是遵循上面的规律:

[X]>[X]{如果是正数,则不变如果是负数,符号位不变,数值位取反加一[X]_原->[X]_补\begin{cases} 如果是正数,则不变\\ 如果是负数,符号位不变,数值位取反加一 \end{cases}

[X]>[X]:连同符号位整体取反加一[X]_补->[-X]_补:连同符号位整体取反加一

思考:为什么对于补码,会比原码多表示一个数,那个数的由来是什么?

我们知道对于位数确定的数值串,表示的数值数量是固定的,就是2^n中排列组合,那么和原码比,为什么补码会多表示一个数?实际上是因为在原码中0,0000和1,0000都表示0,即一个0的情况占据了两种数值串,然而在补码中,0就一种表示方法,[+0]补=[-0]补=0,0000(因为+0的补码就是+0的原码0,0000,所以根据补码的规律,-0的补码就是+0的补码整体取反加一为10,0000,但是此时我们知道溢出了,丢弃最高位就是0,0000所以补码中0的表示情况就只有0,0000)也就是说,此时在补码中空出了一种情况1,0000按照补码的解码规则,在小数中他表示的是-1,在整数中他表示的是-2^n。

反码

我们知道由原码到补码的过程是取反加一,而作为中间过渡结果,取反后的表示就是反码表示。

对于纯小数的反码,我们规定规则如下:

满足以下规律:

[X]>[X]{对于正数,[X]=[X],即无变化对于负数,原码符号不变,数值部分按位取反[X]_原->[X]_反\begin{cases} 对于正数,[X]_原=[X]_反,即无变化\\ 对于负数,原码符号不变,数值部分按位取反 \end{cases}

我们发现实际上和由原码转换成补码的过程相比就少了一个加一而已。并且上面的规则同样适用于反码转换成原码。

和原码表示的数值数量一样。

对于纯整数的反码,我们规定规则如下:

满足以下规律:

[X]>[X]{对于正数,[X]=[X],即无变化对于负数,原码符号不变,数值部分按位取反[X]_原->[X]_反\begin{cases} 对于正数,[X]_原=[X]_反,即无变化\\ 对于负数,原码符号不变,数值部分按位取反 \end{cases}

和原码表示的数值数量一样。

原码、反码、补码之间的转换

我们要能看懂上面这张转换表,实际上很好看懂,对于原码永远都是符号位+数值位的拼接所以有正0和负0两种情况,对于补码,正数部分和原码没有区别,只是补码对于0000000既可以解释为正0也可以表示为负0,而负数部分就是原码的数值位取反+1或者正数补码整体取反加1。而反码正数部分和原码也没有区别,负数部分就是原码的数值位取反,或者正数反码整体取反,或者补码-1。

我们以126和-126为例如何求出他们对应的原码,反码,补码:

对于126

  • 原码就是0111 1110
  • 反码和原码相同就是0111 1110
  • 补码也和原码相同就是0111 1110

对于-126

  • 原码就是1111 1110

  • 反码就是原码数值位取反1000 0001

    或者126的反码(0111 1110)整体取反1000 0001

    或者-126的补码(1000 0010)减一1000 0001

  • 补码就是原码数值位取反加一1000 0010

    或者126的补码(0111 1110)整体取反加一(1000 0010)

    或者反码加一1000 0010

反过来,对于0111 1110和1000 0010看成不同的码又如何求出真值:

对于0111 1110

  • 原码就是直接拼接是0,1111110=+126
  • 因为符号位0是正数,所以反码同原码真值为+126
  • 因为符号位0是整数,所以补码同原码真值为+126

对于1000 0010

  • 原码就是拼接1,0000010=-2

  • 因为符号位1是负数,所以转换成原码时是数值位取反1,1111101就是-125

    或者转换成其对应的正数的反码(也就是正数的原码)就是整体取反0,1111101在改变符号位为负变成1,1111101就是原码了真值就是-125

  • 因为符号位1是负数,所以转换成原码时有效位先-1变成1000 0001再取反1,11111110就是-126

    或者转换成其对应的正数的补码(也就是正数的原码)就是整体先减1再取反0111 1110再改变符号位为负就是1,1111110就是-126

所以我们可以总结出一个规律,对于真值转码按照规则转写即可,但是码转真值都是统一转换成原码再求解真值(因为原码符合人类思维的真值计算)

这里我们给出一个关系图:

移码

移码常用来表示浮点数的阶码,它只能用来表示整数。移码就是在真值X上加了一个常数(偏置量),通常这个常数取2^n(这里的n表示的是有效信息位,一般在有符号数的二进制串中我们把位数说成n+1,n单独表示有效信息位的个数)。即

[X]=2n+X[X]_移=2^n+X

比如:

移码有如下几个特点:

  1. 移码中零的表示是唯一的,[+0]移=2^n+0=[-0]移=2^n-1=100…00(n个0)。

  2. 一个真值的移码和补码只是符号位相反,即在移码中1表示正,0表示负,而原码,反码,补码都是0表示正,1表示负

  3. 移码全0时,是真值的最小值为-2^n,移码全1时,对应真值最大值2^n-1。

  4. 移码保持了数据原有的大小顺序,移码大真值就大,移码小,真值就小

思考:怎么根据移码求得真值?

我们知道

[X]=2n+X[X]_移=2^n+X

所以有

X=[X]2nX=[X]_移-2^n

所以对于移码为0111 1110我们先转换成无符号数的真值126,而2^7=128,所以真值就是126-128=-2或者0111 1110-1000000=1111 1110对应补码真值-2。

所以对于真值转移码按照公式计算,而移码转真值同样是按照公式计算,但是要注意最后的细节问题(例如都转成真值计算时是转换成无符号数,用码计算后转真值时是按照补码规则计算)

总结

这部分内容很难理解,计算较难,重点是要掌握真值和码相互转换的方法。不会了可以看看上面的例题和转换公式。

定点数的运算

移位运算

我们知道在数学计算中经常会形容小数点左移或者右移几位,比如

但是在计算机的定点表示中我们不可能移动小数点,所以我们采取的就是数据移位,移位分为两种:①算术移位,算术移位的对象是有符号数②逻辑移位,逻辑移位的对象是逻辑代码,又可视为无符号数

算术移位

要注意算术移位中因为采用的是有符号数,所以移位后必须保证正负不改变,所以只是数值位移位,符号位不参与移位。

我们知道对于原码,反码,补码的正数部分,即符号位是0的部分,他们的真值是一样的,所以都是移位后空位以0添之。其效果相当于左移乘以2右移除以2(类似于10进制左移乘以10,右移除以10),但是由于原码,反码,补码的负数部分表示有差异,所以移位到底是补0还是补1是不同的,规律见下表:

思考:上面的规律是怎么来的?

我们知道负数的原码部分和正数的原码相同,都是数值位就是真值的绝对值,所以只要移位过程中,符号位不变,补0即可。而观察负数的反码,他除符号位其他的数值位刚刚好和负数的原码相反,所以反码移位填1相当于原码的移位填0。而分析由原码到补码的过程我们发现,当对其由低位向高位找到第一个1时在此1的左边即高位的各位与对应的反码相同,而此1的右边的各位(包括这个1)即低位都与原码相同。所以左移时产生低位填的代码要与原码移位规则相同,所以左移填0,而右移时产生高位,所以右移时代码要与反码移位规则相同,所以右移填1。

对于原码,我们一定要关注的是左移和右移是否会丢1,如果左移那么必定丢位,如果丢的是0不是1,那么计算正确,就是扩大两倍,但是如果左移丢1了,那么计算就会出错误,不是扩大两倍了,右移时如果丢0那么就是缩小两倍,但是一旦丢1,那么精度就不准确了,大约缩小两倍,但是不准确比如上图中的53/2=26,主要是因为后面的小数部分无法表示导致的。

逻辑移位

机器采用的是无符号数,这个就简单多了,就是如下规律:

  • 逻辑左移时,高位移丢,低位添0
  • 逻辑右移时,低位移丢,高位添0

所以01010011逻辑左移1位就是10100110,逻辑右移一位就是00100110,最终结果就是左移扩大两倍,右移缩小两倍。

思考:为什么欲要实现左移扩大两倍,右移缩小两倍,算术移位比逻辑移位复杂很多?

究其原因就是因为有符号数在移位时符号位不动导致的各种特殊情况产生,而逻辑移位由于是无符号数一定是正数就简单了许多。

循环移位

循环移位主要的作用不是用来计算的,而是将数据的低字节数据和高字节数据互换用的,一般有四种情况,主要的特点就是移出的数位会再移入到数据中,而是否带进位就要看是否将进位标志位也加入到循环移位中去。所以有如下四种情况:

对应着的结果是

  • a:11011010
  • b:11011010(注意虽然结果相同,但是移位的方法是不同的)
  • c:11101010
  • d:01101011

加减运算

首先我们要明确加减运算一般只发生在原码和补码中,原码计算复杂,一般不用,补码能够计算出正确结果,并且所有的加减运算最终实际上都是转换成了补码的加法运算。反码一般不用来进行计算,移码更不用说。

原码定点数的加减运算

  1. 加法规则:先判断符号位,如果符号位相同,则绝对值相加,符号位不变。㘝符号位不同,那就是减法,绝对值大的减绝对值小的,并且符号位跟绝对值大的。
  2. 减法规则:两个原码表示的数相减,首先将减数符号取反,然后将被减数与符号取反后的减数按原码加法进行运算。

一定要注意,当左边位出现溢出时,那就直接丢位

补码定点数的加减运算

因为补码加减运算计算规则简单且结果准确,所以计算机常使用补码加减运算。其加减运算需要满足以下特点:

  1. 参与运算的两个操作数都需要是补码表示

  2. 按2进制运算规则得2进1

  3. 符号位和数值位按照同样的规则一起计算,符号位运算产生的进位要丢掉,结果的符号位由运算得出

  4. 补码的加减运算依据下面的公式,当参加运算的数是定点小数时,模M=2;当参加运算的数是定点整数时,模M=2^n+1。(注意,n是数值位长度,所以n+1就是代码的长度)。mod M是为了将溢出位丢掉。

    [A+B]=[A]+[B](modM)[A+B]_补=[A]_补+[B]_补(mod M)

    [AB]=[A]+[B](modM)[A-B]_补=[A]_补+[-B]_补(mod M)

计算实际上很简单,我们以一道例题讲解:

设计器字长是8位(含1位符号位),A=15,B=24,求解A+B和A-B。

A=+15=00001111=[A]补,B=+24=0011000=[B]补,所以

A+B=[A]+[B]=[A+B]=0001111+00011000=00100111=+39A+B=[A]_补+[B]_补=[A+B]_补=0001111+00011000=00100111=+39

AB=[A]+[B]=00001111+11101000=11110111=9A-B=[A]_补+[-B]_补=00001111+11101000=11110111=-9

这里要自己计算出-B的补码,实际上就是B的补码整体取反加1,并且最终得到的结果代码都是补码,我们还需要自己将结果补码转换成真值。

符号扩展

首先我们要明确符号和移位运算是不同的,移位运算是一种为了扩大基数倍移动位数的计算方法。而符号扩展虽然也会增加位数填0或者1,但是其根本目的是将一个真值X的短码扩展至为长码,表示的真值最终应该不发生任何变化。比如一个8位机器字长的数和一个32位数进行相加,那么我们首先需要将8位的数进行符号扩展扩展为32位才能进行加减运算,这就是符号扩展。那么符号扩展的填值同样有自己的规律:

  1. 对于正数部分,仍然是统一的因为原码,反码,补码都一样附加位全部填0即可

  2. 对于负数部分,因为表示方法和解码放手不同,附加为的填值也不同:

    原码:附加位全部填1

    反码:附加位全部填1

    补码:如果是整数附加位填1,如果是小数,附加位填0

思考:符号扩展的正数附加位填0真值不变,移位运算正数也是填0为什么就是扩大真值?

我们要注意符号扩展附加位填0只是将先出现的附加我进行填值,归根结底这些附加位是机器字长的扩大导致的,不是自身数值位移位导致的。而移位运算的添加0的位是由数值位自己左移或者右移“腾”出来的位置再填0才导致的扩大真值,所以我们要加以区分这两者的区别。

我们以一道例题讲解:假设机器字长为8位(含一位符号位),A=15,B=-24,求[A+B]补,[A-B]补。

A=0,1111=0,0001111A_补=0,1111=0,0001111

B=1,01000=1,1101000B_补=1,01000=1,1101000

我们符号扩展以后再根据补码的加减运算规则进行计算:

[A+B]=[A]+[B]=0,0001111+1,1101000=1,1110111[A+B]_补=[A]_补+[B]_补=0,0001111+1,1101000=1,1110111

所以A+B的原码是1,0001001,真值就是-9

[B]=[B]整体取反加一=0,0011000[-B]_补=[B]_补整体取反加一=0,0011000

[AB]=[A]+[B]=0,0001111+0,0011000=0,0100111[A-B]_补=[A]_补+[-B]_补=0,0001111+0,0011000=0,0100111

因为补码是整数,所以这个同时也是原码,所以真值就是+39

溢出判断

我们再计算两个式子,假设A=15,B=-24,C=124。那么此时计算[A+C]补和[B-C]补。

我们会发现结果计算错误了,为什么?我们看一下计算过程,发现A+C时数值位无法表示这个更大的正数绝对值了,从而进1使得1占了符号位导致结果变成负数,这种因为两个正数相加而导致结果过超出了最大正数值表示范围我们成为上溢。而B-C可以看成两个负数-24和-124相加,我们发现两个符号位相加后产生了一个新的符号位0而后面的数值位相加又没有产生进位1导致了结果变成了正数,这种两个负数相加而导致结果超出了最小负数值表示范围的我们成为下溢。

思考:那么难道只要是两个负数相加就一定下溢吗?因为符号位两个1相加一定产生0。

不对的,一定要亲手距离进行计算才能感受到补码负数相加仍是负数的过程。我们来计算一下-1和-2相加。-1的补码是1,1111111,-2的补码是1,1111110,那么-1-2的补码是

[12]=1,1111111+1,1111110=1,11111101[-1-2]_补=1,1111111+1,1111110=1,11111101

那么原码就是1,0000011刚好是-3,也就是说如果两个负数相加没有下溢,那么虽然符号位1+1会产生0,但是恰巧此时的补码数值位相加会进位产生一个1占用符号位导致结果仍然是一个负数从而不会下溢。而下溢的原因就是因为数值位相加没有产生进1从而导致的。

思考:什么时候会产生溢出?

实际上只有两种情况,会产生,就是正数+正数(也就包括了正数-负数,因为补码都是按照加法进行计算的,这里的正+正是补码正+补码正)上溢以及负数+负数(也就包括了负数-正数)。一句话就是同号相加或者异号相减才会导致溢出。

判断溢出的方法一:采用一位符号位

我们发现无论是上溢还是下溢,最终都是操作数补码符号相同结果补码不同的情况,即设

As:操作数一的补码符号位数值A_s:操作数一的补码符号位数值

Bs:操作数二的补码符号位数值B_s:操作数二的补码符号位数值

Ss:结果数的补码符号位数值S_s:结果数的补码符号位数值

规定{AsBsSs=AsBsSs即三个符号位相与As+Bs+SS=AsBsSs即三个符号位相或As,Bs,Ss表示取非即符号位取反规定\begin{cases} A_sB_sS_s=A_s∩B_s∩S_s即三个符号位相与\\ A_s+B_s+S_S=A_s∪B_s∪S_s即三个符号位相或\\ \overline{A_s},\overline{B_s},\overline{S_s}表示取非即符号位取反 \end{cases}

那么我们可以定义

V=AsBsSs+AsBsSsV=A_sB_s\overline{S_s}+\overline{A_sB_s}S_s

当V=0的时候表示无溢出,V=1的时候表示有溢出。我们思考一下上面的式子,发现只有110或者001的时候会出现V=1也就是所说的操作数符号相同但是结果符号不同时溢出此时刚好V=1。

判断溢出的方法二:采用双符号位

我们发现上面的一号位检验只能检测出是否发生了溢出,具体是上溢还是下溢不能检测出来,所以就有了方法二。方法二的双符号位也称模4补码,实际上检测方法很简单,此时的符号位有两位,就是对于结果的溢出符号位进行观察。我们还是以A=15,B=-24,C=124为例,我们观察他们最终结果的补码:

[A+C]=00,0001111+00,1111100=01,0001011[A+C]_补=00,0001111+00,1111100=01,0001011

我们发现对于上溢,实际上是双符号位的00变成了01,此时就是上溢。

[BC]=01,1101000+01,0000100=10,1101100[B-C]_补=01,1101000+01,0000100=10,1101100

对于下溢,实际上是双符号位的01变成了10,此时就是下溢。

而对于不溢出的情况:

[A+B]=00,0001111+00,0011000=00,0100111[A+B]_补=00,0001111+00,0011000=00,0100111

[12]=01,1111111+01,1111110=11,11111101[-1-2]补=01,1111111+01,1111110=11,11111101

无论是正+正,还是负+负刚好两个符号位相同。

所以我们得到如下规律:

Ss1:结果补码的第一个符号位数值,Ss2:结果补码的第二个符号位数值S_{s1}:结果补码的第一个符号位数值,S_{s2}:结果补码的第二个符号位数值\\

{Ss1Ss2=00:结果为正数,无溢出Ss1Ss2=01:结果正溢出(上溢)Ss1Ss2=10:结果负溢出(下溢)Ss1Ss2=11:结果为负数,无溢出\begin{cases} S_{s1}S_{s2}=00:结果为正数,无溢出\\ S_{s1}S_{s2}=01:结果正溢出(上溢)\\ S_{s1}S_{s2}=10:结果负溢出(下溢)\\ S_{s1}S_{s2}=11:结果为负数,无溢出 \end{cases}

为了更加简单的判断出是否溢出,我们规定逻辑表达式

V=Ss1Ss2V=S_{s1}⊕S_{s2}

V=0表述无溢出,V=1(01或者10)表示有溢出。

判断方法三:采用一位符号位根据数据位的进位情况判断溢出

我们再观察一下这两种溢出情况的特点,发现上溢时是之前的符号位为0,然后最高位数的进位是1,而下溢时是之前的符号位为1,然后最高位数的进位是0。即符号位和最高数位的进位总是相反的,而对于无溢出的情况,要么最高数位不产生进位(比如正+正不溢出)或者最高数位产生的进位和符号位相同(比如负+负不溢出)。


所以逻辑表达式如上图。我们对比一下这三个方法,我认为还是方法二比较简洁易于理解。但是无论是哪个方法,归根结底都是V=0无溢出,V=1有溢出。

乘法运算

乘法运算有两种计算方法,这里我们分别讲解:

方法一:原码一位乘法

顾名思义,是使用原码进行乘法计算,这很符合我们人类的思维,但是又有一些变化。原码一位乘法的特点是符号位和数值位要分开求,首先我们单独判断一下结果的正负,这个可以使用乘数和被乘数取异或操作即可得到结果的正负。那么接下来重点是我们要看一下数值位(那么就必定是绝对值了,正数)的乘法操作了。首先我们参考一下规则:

  1. 被乘数和乘数均取绝对值参加运算,符号位为异或即可
  2. 部分积的长度同被乘数,取n+1位,以便存放乘法过程中绝对值大于等于1的值,初值为0.
  3. 从乘数的最低位开始判断,若最低位为1,那么部分积加上被乘数,然后逻辑右移一位(即补0),若最低位为0,那么部分积加上0然后右移一位。
  4. 重复步骤3n次即可得到正确结果。

注意:考虑到运算时可能出现绝对值大于1的情况(但是此刻并没有溢出),所以部分积和被乘数都采用双符号位。

相比你也和我一样看蒙了,没关系,我们实际操作一下便可轻易理解。

设x=-0.1101,y=0.1011,采用原码一位乘法求解x*y。我们首先用我们之前学过的乘法计算一下:

那么计算过程如上图所示,可以看出乘数的某一位位权决定被乘数是否本次加上乘数,然后4个每次的中途计算结果都要左移一位,最后累加4个中间结果得到一个2倍被乘数长的结果。但是我们知道计算机是不可能自己理解这种计算方法的,单单中途结果左移就很难实现,所以我们使用了上面的计算规律:

可以看出,对于每一次高位部分积是否加上乘数主要取决于乘数的最后一位。而每次的中间结果和乘数都要右移,但是中间结果是逻辑右移,而乘数每次右移后新增加的高位要添加中间结果逻辑右移时丢失的地位,因为最后还要用到这些中间结果右移丢失的地位来组成低位部分积,这样当乘数全部右移出去以后,存储乘数的寄存器里存储的就是低位部分积了,最终高位部分积和低位部分积拼接即得到了最终结果的原码。

为了更加容易理解上面这段话我们来用图示的方法演示一遍:

首先第一步1101最后一位是1所以高位部分积加一次被乘数:

然后中间结果逻辑右移同时乘法寄存器中乘数右移,然后中间结果的丢弃低位保存到乘数寄存器的空高位

然后继续乘,此时乘数寄存器中的乘数还剩三位101且最后一位是1所以还要进行高位部分积加被乘数(一定要注意此时的乘数寄存器中的最高位不是乘数的一部分了,而是之前中间结果的丢弃的低位1,实际上后来这个位1会用来组成低位部分积):

然后再次将中间结果逻辑右移,乘数也右移,并且同时中间结果的最低位1移到乘数的空高位:

然后继续乘,此时乘数寄存器中的乘数还剩两位10且最后一位是0所以高位部分积进行加被0(一定要注意此时的乘数寄存器中的高2位不是乘数的一部分了,而是之前中间结果的丢弃的低位,实际上后来这2个位会用来组成低位部分积):

然后中间结果再次逻辑右移,同时乘数右移,再将中间结果右移丢弃的低位移到乘数寄存器的空高位:

然后继续乘,此时乘数寄存器中的乘数还剩一位1且最后一位是1所以还要进行高位部分积加被乘数(一定要注意此时的乘数寄存器中的高3位不是乘数的一部分了,而是之前中间结果的丢弃的低位,实际上后来这3个位会用来组成低位部分积):

然后中间结果逻辑右移,乘数右移,然后中间结果的丢弃低位移到乘数寄存器的空高位:

这样我们就通过原码一位乘法完成了计算,再加上符号是负号所以最终结果就是-0.100001111。我们最后来总结一下这个过程,发现实际上高位部分积寄存器中的每一次逻辑右移就相当于将中间结果左移了,乘数寄存器的每一次右移就相当被乘数下一次乘以乘数的前一位。而为什么高位部分积寄存器中中间结果右移的丢弃位要用掉乘数寄存器中呢?实际上是因为这个被丢弃位就是低位部分积的组成部分需要保留下来以便后面拼接出最终结果,而刚好可以存储到乘数寄存器中,这样就充分利用了空间,使得计算过程只需要两个寄存器即可完成。现在我们再对比之前的纸质乘法图就可以轻易理解原码乘法规则了。

方法二:补码一位乘法(Booth算法)

我们想一想上面的原码一位乘法是一种符号单独拿出来确定,然后后面的数值就是无符号数的绝对值之间进行乘法运算,而接下来我们的补码一位乘法运算时将补码中的符号位连带着数值位一起进行计算最终的结果同时表示正负和数值。所以这是一种有符号数的乘法,采用相加和相减操作计算补码的乘积。设

[X]=xsx1x2...xn;[Y]=ysy1y2...yn[X]_补=x_sx_1x_2...x_n;[Y]_补=y_sy_1y_2...y_n

运算规则如下:

  1. 符号位参与运算,运算的数均以补码表示

  2. 被乘数一般选取双符号位参与运算,部分积取双符号位,初值为0,乘数可以去单符号位。

  3. 乘数末尾新增设一个附加位yn+1,且初值为0

  4. 根据(yn,yn+1)的取值来确定操作,见下表:

  5. 移位按补码右移规则进行

  6. 按照上述算法进行n+1步操作,但第n+1步不再移位(共进行n+1次累加和n次右移),仅根据yn和yn+1的比较结果做相应的运算

  7. 最终取高位部分积的低n位和低位部分积(就是乘数寄存器中的高n位)组成最终结果

我们还是求解上面的例题:设x=-0.1101,y=0.1011,采用补码一位乘法求解x*y。

最终我们拼接得到结果是1.01110001,但是要注意这是补码,如果是正数那么就直接可以看成原码求真值了,但是现在还是补码,我们还要将它转换成原码最终结果是-0.10001111。

思考:两种方法的异同点?

乘法类型 符号位参与运算 部分积位数 乘数位数 累加次数 移位方向 移位次数 每次移多少位
原码一位乘法 2位 0位 n n 1
补码一位乘法 2位 1位 n+1 n(最后一次累加后不一位) 1

一定要注意原位一位乘法每此累加必移位,而补码一位乘法最后一次累加补移位,无论是哪种方法最后乘数寄存器抛弃掉了乘数的所有位,并且都是高位部分积的逻辑右移低位放到乘数寄存器的空高位。最终的结果都是高位部分积和低位部分积拼接而成,并且一定是2倍的被乘数长度。

除法运算

我们再来讲一下定点数的除法运算,除法运算同样有两种方法:

原码除法运算(不恢复余数法)

又称为原码加减交替除法,特点和原码一位乘法一样,符号和数值分开计算,商的符号由操作数的异或取得,求商的数值规则如下:

设被除数[X]=xsx1x2...xn,除数[Y]=ysy1y2...yn设被除数[X]_原=x_sx_1x_2...x_n,除数[Y]_原=y_sy_1y_2...y_n

  1. 商的符号:被除数和除数的符号异或得到
  2. 商的数值:|Q|=|X|/|Y|。实际上就是绝对值的运算,我们定义除法规则如下:
  3. 先用被除数减去除数(|X|-|Y|=|X|+(-|Y|)=|X|+[(-|Y|]补),当余数为正时,商上1,余数和商左移一位,再减去除数。当余数为负时,商上0,余数和商左移一位,再加上除数。
  4. 当第第n+1步余数为负时,需要再加上|Y|得到第n+1步正确的余数(保证余数和被除数同号,因为是绝对值之间的除法,被除数必定是正的)

我们以一道例题讲解:机器字长为5位(含一位符号位,n=4),x=0.1011,y=0.1101,采用原码加减交替除法求x/y。

一定要注意在最开始先进性一次减去除数,然后再开始商。所以虽然进行了5次操作,但是商只是左移了4位刚好将数值位填满了,商的符号位仍应为正。而上面这到题最终余数刚好为正,所以没有进行最后一步即可。这样我们就求解出来商是+0.1101,余数是0.0111*2^(-4)。这里一定要注意余数后面的级数,为什么会产生2^(-4)呢?因为实际上我们最终得到的只是余数的数值位,他的每一次左移都会缩小2倍,所以就没一次左移(我们发现实际上就是逻辑左移)对余数产生一个*2^(-1)。并且一定要注意虽然说是原码除法,但是实际上操作时加减的除数使用的是补码但是最终结果得到的是原码所以叫原码除法运算即余数和商都是原码。

补码除法运算(加减交替法)

补码法原则的特点就是符号位与数值位一同参加运算,最终拼接的结果自动带正负号。他和原码除法原则最大的不同就是在第一次不是必定减去除数,而是根据被除数和除数的符号决定是加除数还是减除数。并且要注意这种方法需要双符号位。规则如下:

  1. 符号位参加运算,除数与被除数均使用补码形式表示,商和余数也用补码形式表示。
  2. 若被除数与除数同号,则被除数减去除数,若被除数和除数异号,则被除数加上除数。
  3. 若余数与除数同号,那么商1,余数左移一位减去除数,若余数和除数异号,则商上0,余数左移一位加上除数。
  4. 重复步骤3n次(注意字长为n+1次,所以n表示的是数值串长度)
  5. 若对商的精度没有要求,那么最后一次第n+1次,商恒置为1(末位恒置1法)

我们还是以一道例题讲解:机器字长为5位(含一位符号位,n=4),x=0.1000,y=-0.1011,采用补码加减交替法求x/y。采用双符号位表示:

[X]=00.1000,[X]=00.1000[X]_原=00.1000,[X]_补=00.1000

[Y]=11.1011,[Y]=11.0101,[Y]=00.1011[Y]_原=11.1011,[Y]_补=11.0101,[-Y]_补=00.1011

注意到此时使用的是双符号位,并且对于字长为5,执行了5次加减操作,4次移位,并且此时商和余数也都是逻辑左移。最终的商的补码是1.0101所以原码是1.0101也就是-0.0101,余数的补码是00.0111,为正数,所以原码也是00.0111,所以余数就是0.0111*2^(-4)。一定不要忘记逻辑左移会让余数每次都乘一个2^(-1)即缩小两倍。

思考:两种方法的异同点?

乘法类型 符号位参与运算 加减次数 移位方向 移位次数 说明
原码除法
(不恢复余数法)
N+1或者N+2 N 若最终余数为负,需要恢复余数,所以加减次数为N+1或N+2
补码除法
(加减交替法)
N+1 N 商末位恒置1

我们发现对于原码的乘法或除法通常都是一符号位即可,因为计算时肯定是正数,小数点前面的符号位主要是用来暂存移位时产生的进位的。而对于补码的乘法或除法运算,通常需要两个符号位,这是因为一个符号位需要用来表示正负,还有一个是用来暂存进位结果的。并且无论是哪种乘法或者除法运算,一定是移位n次,但是加减次数却是不同的。且一定要注意对于原码的乘法和除法运算结果就是原码可以直接计算真值,但是对于补码乘法和除法运算结果都是补码,如果是正数那么可以直接计算真值,但是如果是负数一定要记得先转化成原码再计算真值,并且对于除法运算,余数会有一个级数不要忘记。

强制转换

在考纲中还要求我们要对高级语言高级程序设计语言(如C)的语言变量的类型强制转换有深入的掌握,所以接下来我们学习一下两种常见的强制转换。

有符号数和无符号数的转换

我们观察下面的C代码

1
2
3
4
5
6
7
8
9
10
#include<bits/stdc++.h>
using namespace std;

int main(){
short x=-4321;
unsigned short y=(unsigned short)x;
printf("x=%d, y=%u\n",x,y);
system("pause");
return 0;
}

他最终的输出结果为

1
x=-4321, y=61215

和我们预期的效果不同,我们肯定是希望类型转换以后表示的值是不变的,但是我们知道无符号数是无法表示一个负数的,所以最终结果变成了一个我们“意料之外”的值。但是我们仔细观察一下此时两个数x,y的二进制数:

变量 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
x -4321 1 1 1 0 1 1 1 1 0 0 0 1 1 1 1 1
y 61215 1 1 1 0 1 1 1 1 0 0 0 1 1 1 1 1

实际上x和y的二进制代码是一样的,我们前面也见过对变量不同的声明并不会改变代码,而只是改变了对于一个二进制码的解读方式,例如上面的x会被解释为一个-4321的补码,而y则会直接按照原码进行解释。这也就造成了当强制转换以后对于同一个二进制代码的解读方式发生了改变从而导致了值的异常改变。但是我们知道了这个无符号数和有符号数的强制转换方法以后就可以预测到强制转换后的数值会变成多少。比如:

1
2
3
4
5
6
7
8
9
10
#include<bits/stdc++.h>
using namespace std;

int main(){
unsigned short x=65535;
short y=(short)x;
printf("x=%u, y=%d\n",x,y);
system("pause");
return 0;
}

那么我们猜测以下x,y的真值会是多少。首先我们写出x的二进制代码,我们知道short和unsigned short都是2B,占16位,所以我们可以写出x的原码是:

变量 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
x 65535 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

那么当转换成有符号数以后由于最高位是-1所以是负数,那么就会将上面这个二进制代码解读为y的补码,所以我们可以推出y的原码,就是在补码的数值位减1后逐位取反得

变量 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
y -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1

所以y的原码如上我们可以计算出真值就是-1,我们用机器验证一下得到结果却时是我们预测的值:

这就是无符号数和有符号数之间的强制转换方法。

不同字长整数之间的转换

我们知道有时候还会有字长的强制转换,比如长数值类型转换成短数值类型如下:

1
2
3
4
5
6
7
8
9
10
11
#include<bits/stdc++.h>
using namespace std;

int main(){
int x=165537,u=-34991;
short y=(short)x,v=(short)u;
printf("x=%d, y=%d\n",x,y);
printf("u=%d, v=%d\n",u,v);
system("pause");
return 0;
}

最终的结果如下:

我们发现数值发生了很大的变化,并且连正负号都发生了变化,这是为什么?我们用16进制输出一下上面各值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bits/stdc++.h>
using namespace std;

int main()
{
int x = 165537, u = -34991;
short y = (short)x, v = (short)u;
// printf("x=%d, y=%d\n",x,y);
// printf("u=%d, v=%d\n",u,v);
cout << hex << setw(8) << setfill('0') << x << endl;
cout << hex << y << endl;
cout << hex << u << endl;
cout << hex << v << endl;
system("pause");
return 0;
}

输出结果:

我们发现他只是将高位部分截取了,这样我们就不难理解为什么会发生数值的正负变化了,肯定是之前的最高位为0所以解释为正数,但是当截断以后最高位变成了1,那那么就自然就解释成了负数。但是这是长->短使用了截断的方法,那么如果是短->长呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <bits/stdc++.h>
using namespace std;

int main()
{
short x = -4321;
int y = x;
unsigned short u = (unsigned short)x;
unsigned int v = u;
printf("x=%d, y=%d\n", x, y);
printf("u=%d, v=%d\n", u, v);
system("pause");
return 0;
}

输出结果:

我们发现对于short类型的短负数x再强制转换为int类型的长负数y以后真值并没有改变,而将x转换为无符号数u以后再转换为长int型v以后数值也没有发生变化,我们再观察一下此时这四个值的16进制代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <bits/stdc++.h>
using namespace std;

int main()
{
short x = -4321;
int y = x;
unsigned short u = (unsigned short)x;
unsigned int v = u;
// printf("x=%d, y=%d\n", x, y);
// printf("u=%d, v=%d\n", u, v);
cout << hex << x << endl;
cout << hex << setw(8) << setfill('0') << y << endl;
cout << hex << u << endl;
cout << hex << setw(8) << setfill('0') << v << endl;
system("pause");
return 0;
}

我们发现对于x->y的负数形式增长时高位全部补得是十六进制的f,也就是说对于二进制代码高位全部补1,而对于u->v的正数形式增长时高位补得是十六进制的0,也就是说二进制的高位补得全部是0。所以我们可以总结出一个规律,对于短->长的代码扩展,新添加的高位补得数和原先代码的最高位一致,这样我们就可以保证增长位数的强制转换以后数的真值没有发生变化了。并且我们还可以总结出只有长整数->短整数的强制转换真值不发生变化,其他情况都是会发生改变的,当然了了,长->短的字长强制转换也有小概率不会发生改变。前三个例子的转换规则都是保证相应的位值相等,而短字长到长字长的转换,在位值相等的条件下还要补充高位的符号位,可以理解为数值的相等。但是注意,char类型为8位ASCII码整数,其转换为int时,在高位部分补0即可。

总结