DATALAB

实验题目:DATALAB

实验目的:填写bits.c文件中尚未完成的各个函数的内容。

1.INTEGER类型函数的编程规则

其中,每一个表达式"Expr"只能使用如下规则:
①数字只能使用0到255(0xff),不能使用像0xffffffff这样大的数字
②函数参数和局部变量(没有全局变量)
③一元运算目:! ~
④二元运算目:& ^ | + << >>

而且,以下行为是禁止的

①使用任何控制结构,如if, do, while, for, switch等。

②定义或使用任何宏。

③在此文件中定义任何其他函数。

④调用任何库函数。

⑤使用任何其他的操作,如&&,||,-,或?:

⑥使用任何形式的casting

⑦使用除int以外的任何数据类型。这意味着你不能使用数组、结构体、共用体等。

2.FLOATING PONIT类型函数的编程规则

对于需要你执行浮点运算的问题,编码规则较不严格。允许使用循环和条件控制也可以同时使用int和unsigned。可以使用任意整数和无符号常量。

上述禁止行为中,除②③④⑥同样适用外,⑦允许使用unsigned,此外禁止使用任何浮点数据类型、操作或常量。

3.阅读README清楚dlc的使用方法

实验环境:16.04 32位Ubuntu系统

实验内容及操作步骤:先填写bits.c文件中尚未完成的各个函数。

1.bitAnd

题目:仅使用~和|完成x&y操作。

分析:直接用德摩根律秒杀,即~(x&y)=~x|~y,x&y=~((~x)|(~y)),故如果不会,可以通过列真值表观察得到。实在不行,也可以通过逻辑推导出来,x&y本质是找x和y中同位置上的1,而~x有x中没有的1,~y同理。因此~x|~y可以理解为找x、y中都没有的1,对此取反,即~((~x)|(~y))就是找x、y都有的1。

代码:

int bitAnd(int x, int y) {

return ~((~x|~y));

}

2.getByte

题目:获得x第n个字节的数字。

分析:首先要知道,字节从右到左分别是第0字节,第1字节......知道字节分布后,分两步。第一步是将x整体右移8*n位(一个字节8位),使第n个字节在最右端。

第二步是只取得该字节,其余字节一律为0。我们只要与上0xff,高24位和0相与必为0,因此可以得到我们想要的第n个字节。

代码:

int getByte(int x, int n) {

return (x>>(n<<3))&0xff;

}

3.logicalShift

题目:将x按逻辑移位向右移动n位。

分析:题目要求的是逻辑移位,但是在使用移位运算符的时候,计算机实现的是算术移位,因此当x是负数的时候,向右位移高位是会补1的。比如0x87654321右移4位,得到的是0xf8765432,

想要得到0x08765432,就要对补位处进行处理。我们可以分两步完成。

第一步是将x右移n位。

第二步是处理补位处,根据上一题得到的灵感,我们可以构造一个可以动态调节的0x0...f与右移后的x相与,二进制下它的0和1都是随n变化的。如例子中0x87654321右移了4位,我们需要0x0...f中有32-4个1,高位有4个0用来处理补位产生的1。因此我们得到了0x0...f的实现公式(1<<32+((~n)+1))+(~0),即2^(32-n)-1,“-”操作用“~”后+1代替,其中-1等价于~0。但我们发现当n=0的时候,会存在特殊情况,此时(1<<32+((~0)+1))+(~0)等价于0,n=0我们需要与上的0xffffffff而不是0,因此我们需要引入一个变量,只在n=0的时候生效。很巧妙的是,我们发现((~(!n))+1)这个变量,当n!=0的时候,它的取值等于-1+1,不会产生影响,当n=0的时候,它的取值等于-2+1=-1,-1=0xffffffff,刚好解决了上述问题。

代码:

int logicalShift(int x, int n) {

int temp1=32+((~n)+1);

int temp2=(1<<temp1)+(~0)+((~(!n))+1);

return (x>>n)&temp2;

}

4.bitCount

题目:数出x二进制下有多少1

分析:利用分治的思想(应该是吧),不去数整个x,而是数x每个字节中有多少1,最后再把每个字节中1的数目加起来。因此可以分成三步。

第一步,创造一个int temp起辅助作用,由于编程规则的限定,temp=0x01010101不能这样直接写,需要temp=1|(1<<8)|(1<<16)|(1<<24)间接获得。

第二步,计数count初始化为0。然后count+=x&temp,取得每个字节最低位1的数量,接着count+=(x>>1)&temp,加上每个字节的次低位中1的数量...以此类推,取完每个字节的8位,此时count的每个字节都存储了x对应字节中的1的数量。

第三步,分别统计count每个字节的值,然后加起来。比如(count&0xff)+((count>>8)&0xff)+...

第一个括号内是count第0个字节中存储1的数量,第二个括号是count第1个字节中存储1的数量,二者加起来,如此类推。

代码:

int bitCount(int x) {

int temp=1|(1<<8)|(1<<16)|(1<<24);

int count=0;

count+=x&temp;

count+=(x>>1)&temp;

count+=(x>>2)&temp;

count+=(x>>3)&temp;

count+=(x>>4)&temp;

count+=(x>>5)&temp;

count+=(x>>6)&temp;

count+=(x>>7)&temp;

   return (count&0xff)+((count>>8)&0xff)+((count>>16)&0xff)+((count>>24)&0xff);

}

5.bang

题目:不用!,用其他运算符来实现!的功能。

分析:分析!的本质,是通过寻找数字中有无1来实现的,如果有1返回0,如果没有1返回1。按照上一题的思路,我们仍可以分字节寻找,但这是候我们发现,我们最后的返回值是0和1,因此我们要转换一下想法,让x本身自己通过|操作寻找。也是采用一种分治的思想。即32位的int,高16位和低16位相或,x=(x>>16)|x,如果x不全为0,即可证明x有1,x的1全部被或到低16位,如0x11010000或完之后是0x11011101,再让低16位中的高8位,或上移位后的x低16位中的低8位,依次类推,这样做的目的是,假如x中有1,经过以上操作,其最低位必为1。此时对最后结果&1,只取最低位,然后^1,取非。0^1=1,1^1=0。

以上思路略微有些复杂,我们可以参考另一条思路。利用补码相反数的性质,除了0与0x80000000之外,其他的数与其相反数的符号肯定是不同的。而0x80000000的相反数是它本身,所以只有0|0的相反数最高位是0,其他数|相反数最高位是1。若

(x|(~x+1))如果不是0,右移31位会得到0xffffffff,如果是0,则会得到0x0,最后+1即可得到!x。

代码:

int bang(int x) {

x=(x>>16)|x;

x=(x>>8)|x;

x=(x>>4)|x;

x=(x>>2)|x;

x=(x>>1)|x;

   return (x&1)^1;

}

6.tmin

题目:返回int最小值。

分析:由常识可知int最小值是-2^31,由于不能直接赋值,那就将1<<31可得0x80000000(补码表示-2^31)。

代码:

int tmin(void) {

return 1<<31;

}

7.fitsBits

题目:如果x可以被n bit的补码表示则返回1否则就返回0。

分析:在没有意识到是符号扩展之前,单纯分析样例也可以揣测出一些门道。比如5的补码为00...0101,而-4的补码为11...1100,5之所以不能被3bit的补码表示,是因为其符号位(0)和有效最高位(1)不同,而-4的符号位(1)和有效最高位(1)均为1,也就是说。基于这个思想,我们需要将x右移n-1位,此时x的末位是应是有效位的最高位,如果这位是0,那么要前面高位均为0(即符号位为0),如果这位是1,那么要前面高位均为1(即符号位为1),这两种情况才能让x被n bit的补码表示。

当意识到这跟符号扩展有关后,我们可以转换一个角度思考。

想要判断一个数是否能用更少的位数n来表示,只需判断该数进行符号扩展(先左移后右移)之后得到的数与原来的数是否相等(用异或来判断),若相等则返回1,否则返回0,本质上还是在寻找我上文提及的两种情况,只是不同实现方法。(移位长度为32+(~n)+1)

代码:

int fitsBits(int x, int n) {

x=x>>(n+(~0));

return !x|!(x+1);

}

8.divpwr2

题目:求x/(2^n),结果向0舍入。

分析:观察注释中的两个例子,15/2=7,-33/16=-2。15/2相当于是(15>>1)=7,(-33>>4+1)=-2,不难看出负数比起正数计算存在不同。因此我们分成两步完成。

第一步,判断正负,flag=x>>31,若x大于0,则flag=0...0,若x小于0,则flag=1...1,我们不需要或上1只取一位的值,flag这样在下一步会起到作用。

第二步,负数计算需要引入偏置值,-33>>4+1相当于(-33+(2^4-1))>>4,先帮-33的第5位向右移动一位,再右移4位。故bais=(x>>31)&((1<<n)+(~0)),若x大于0则bais为0,若x小于0则bais有值。x加上偏置值后右移相应n位即为所求。

代码:

int divpwr2(int x, int n) {

int bais=(x>>31)&((1<<n)+(~0));

    return (x+bais)>>n;

}

9.negate

题目:求-x。

分析:前文提过,“-”操作相当于先“~”再+1。

代码:

int negate(int x) {

return (~x)+1;

}

10.isPositive

题目:正数返回1,0或负数返回0。

分析:先向右移31位,然后与1相与,只取符号位。然后判断x是否是0,不是0则取非返回,是0就返回0。(不是0则!x=1)

代码:

int isPositive(int x) {

int flag=(x>>31)&1;

return !(flag|!x);

}

11.isLessOrEqual

题目:如果x<=y则返回1,否则返回0

分析:分三步。

第一步,先判断x和y的正负,与上文一样(x>>31)&1。

第二步,判断x和y是否同号,!(flag1^flag2),同号为1,异号为0。

第三部,同号和异号分开讨论,y-x相当于y+(~x)+1,用temp取y-x后的符号位!((y+(~x)+1)>>31),如果y-x是0或大于0则temp返回1,而小于0则temp返回0(依旧注意负数会高位补1,因此会返回0)。总而言之,同号并且y-x后大于等于0会返回1,异号则关注x,若x为负则y必为正,正-负大于0返回1。

代码:

int isLessOrEqual(int x, int y) {

int flag1=(x>>31)&1;

int flag2=(y>>31)&1;

int flag3=!(flag1^flag2);

int temp=!((y+(~x)+1)>>31);

return (flag3&temp)|(!flag3&flag1);

}

12.ilog2

题目:求log2(x)的值

分析:由于int是32位的,所以log2(x)=16a+8b+4c+2b+e。并且不难发现,在二进制中,求log2(x)相当于在找x中1的最高位,将数学问题转换为计算机问题就更方便解决了。经历过前面题目的锻炼,找1可以用分治的思想。

32位对半,x>>16,!(x>>16)判断高16位是否有1。

①若有,则!!(x>>16)为1,接着高16位对半,查看高16位的高8位是否有1,若有,则!!(x>>(8+16)))为1,以此类推,直到找到高16位中的1所处位置。

②若无,则!!(x>>16)为0,接着低16位对半,查看低16位的高8位是否有1,若有,则!!(x>>(8+0))))为1,以此类推。

巧妙的是,控制移位位数的变量就是我们所求的值。

代码:

int ilog2(int x) {

int count=0;

count=(!!(x>>16))<<4;

count+=((!!(x>>(8+count)))<<3);

count+=((!!(x>>(4+count)))<<2);

count+=((!!(x>>(2+count)))<<1);

count+=((!!(x>>(1+count)))<<0);

return count;

}

13.float_neg

题目:返回浮点数uf的负数形式-uf。

分析:我们知道,浮点数是这样表示的V=(-1)^s*2^E*M

而浮点数值又可分为三种,规格化,非规格化,特殊值(无穷大和NaN)。

 

因此我们对于uf返回其负数形式需要进行分类讨论,其中只有NaN是不能通过修改符号位来获得其负数形式,因此我们只需要找出NaN,让NaN返回其本身,其他数直接修改符号位即可。对于无穷大和NaN两种情况,它们的exp都是11111111,但无穷大的frac是0,NaN的frac不等于0,无穷大可以直接修改符号位得到其负数形式,而NaN需要返回其自身,因此我们需要分辨出两者。我们先将uf<<1舍去符号位,然后和0xffffffff异或,如果异或后的结果小于<0x00ffffff,说明我们uf是NaN,返回其本身。如果不是,则进入下一个else语句,即uf^0x80000000,直接修改符号位为1。

代码:

unsigned float_neg(unsigned uf) {

if(((uf<<1)^0xffffffff)<0x00ffffff) return uf;

else

return uf^0x80000000;

}

14.float_i2f

题目:将int x转换成float x。

分析:这一题比较麻烦,主要体现在int转float的舍入问题上。先讲思路,由上一题我们可知浮点数在计算机中的存储方式。我们需要将从x中找到浮点数所需要的s,exp和frac。因此我们先将int分成0,+,-三种。

①x=0,可以直接返回0,不需要做多余操作。

②判断正负,如果x是负,移除其符号位,即tempx=-x,用s保存其符号位(默认情况s=0),方便后续得到exp和frac。

③现在无论正负都是无符号数,我们可以对其进行处理。

首先我们要得到x的阶码,可以将tempx不断左移直至有效数字部分到达最高位,记录下左移的次数,我们通过32-左移次数,就能知道E的大小。这里有一个特别注意的点,写在代码注释上了,这里简单说说就是frac是不包含m小数点前面的1。

然后我们要得到x的frac部分,注意int有32位,而浮点数的frac只能存储23位,所以最多会有9位会被舍去,因此我们要考虑到舍入问题。我们通过一个例子,讲明白浮点数的舍入问题。

例如有效数字超出规定数位的多余数字是1001,它大于超出规定最低位的一半(即0.5),故最低位进1。如果多余数字是0111,它小于最低位的一半,则舍掉多余数字(截断尾数、截尾)即可。对于多余数字是1000、正好是最低位一半的特殊情况,最低位为0则舍掉多余位,最低位为1则进位1、使得最低位仍为0(偶数)。这就是浮点数舍入的标准,向最接近的值舍入,也称向偶数舍入。

因此代码实现时,我们要分以下三种情况。第一,如果末尾9位大于100000000,那么舍入后面9位必会进1。第二,如果末尾9位刚好等于100000000,这时是否进位得看第10位,如果10位为1,满足浮点数向偶数舍入的原则,那么舍入后面9位可以进1。第三,不满足上述条件的,直接被舍去,不向第10位进1。

最后我们把得到的数据总结一下,exp等于32-左移次数+127(偏置值),frac等于左移后的tempx再左移9位+flag(舍入部分是否进1)。

代码:

unsigned float_i2f(int x) {

unsigned left_shift=0,s=0,tempx=x,flag,after_shift,tmp;

if(!x) return 0;

if(x<0){

s=0x80000000;

tempx=-x;

}

after_shift=tempx;

while(1){

tmp=after_shift;//用tmp的原因是,要借助tmp将判断跳出的时机往后延

after_shift<<=1;//一位,即after_shift有效数字最高位1被左移掉之后,

left_shift++;//才跳出,这时left_shift++,才能保证frac中不包含m

if(tmp&0x80000000) break;//小数点前面的1。

}

if((after_shift&0x01ff)>0x0100) flag=1;

else if((after_shift&0x03ff)==0x0300) flag=1;

else flag=0;

return s+(after_shift>>9)+((32-left_shift+127)<<23)+flag;

}

15.float_twice

题目:返回两倍的uf。

分析:还是老样子,浮点数有三种的数值分类,因此分类讨论。

①阶码全为1,即exp=11111111,此时uf不是无穷大就是NaN,乘以2无意义,返回参数自身。

②阶码全为0,这时候浮点数是非规格化的浮点数,直接左移一位即可。

③除上述两种之外的规格化浮点数,直接阶码+1即可。

代码:

unsigned float_twice(unsigned uf) {

unsigned s=uf&0x80000000;

unsigned exp=uf&0x7f800000;

unsigned frac=uf&0x007fffff;

unsigned f=uf;

if(exp==0) f=(frac<<1)|s;

else if(exp!=0x7f800000) f=f+0x00800000;

return f;

}

实验结果及分析:

1.使用dlc检测bits.c是否有错。

 

2.使用dlc -e操作查看操作数。

 

3.通过make执行Makefile文件,对当前目录下所有.c文件进行编译。然后使用

./btest bits.c验证所有函数的功能是否正确。

 

 

4.测试结束后或者每次用btest测试时,需使用make clean删除生成的可执行文件。

 

5.如果某个函数存在错误,可以使用./btest –f (函数名),例如./btest –f float_twice,来测试某特定的函数。

 

6.也可以使用./ishow和./fshow显示整数和浮点数的位级表示,用来思考自己函数哪里写错了。

 

收获与体会:

1.通过该实验进一步熟悉了整型及浮点数的位表达形式,实现常用二进制运算的常用方法。

2.对基本的二进制运算的总结

按位与(&):参与运算的数字转换为二进制,而后逐位对应进行运算。

按位或(|):参与运算的数字转换为二进制,对应位相或。

异或(^): 参与运算的数字转换为二进制,对应位相异或,规则两位相同为0,不同为1。异或运算能够实现翻转,高效交换两个变量的值等功能。

按位非():将一个数按位取反,即0 = 1,~1 = 0。

逻辑非(!):将真值结果取反,如!5=0,!0=1。

3.算术移位和逻辑移位的区别

左移:x<<y表示将x左移y位,左边的位全部丢弃,在右边全部补0。

右移:x>>y表示将x右移y位,右边的位全部丢弃,对于逻辑移位,左边补0,对于算术移位,左边填充符号位。

4.实验中感觉分治法思想很适合二进制运算,将多个字节的问题分解为单个字节,再分解到具体的几位。如果某个函数出现问题,比如浮点数类,一定要考虑到所有的数值分类,根据报错结果,借助fshow工具来发掘问题。不过整数向浮点数的舍入真的要注意是后9位,哪怕其实int中最多只有7位会舍掉(除去了符号位和m小数点前面的1),但是32位的int保留到23位float中的frac还是要考虑9位。此外,“-”可以用“~”+1来取代,截取某个片段可以&上某个数字来获取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值