实验题目:DATALAB |
实验目的:填写bits.c文件中尚未完成的各个函数的内容。 1.INTEGER类型函数的编程规则 其中,每一个表达式"Expr"只能使用如下规则: 而且,以下行为是禁止的 ①使用任何控制结构,如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。 分析:我们知道,浮点数是这样表示的 。而浮点数值又可分为三种,规格化,非规格化,特殊值(无穷大和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来取代,截取某个片段可以&上某个数字来获取。 |
DATALAB
最新推荐文章于 2024-11-08 14:34:47 发布