计算机系统基础实验1(二进制操作)
一、知识储备
在具体实验之前,放一些知识储备和实验要求,以备更好地理解实验原理,更好地了解二进制操作,有了这些基础那么写实验就可以放飞思路了。
A.补码
- 所有的数据在计算机中都是以补码形式储存的,正数不必多说,负数是将一个数的原码取反加1,最高位为符号位
- 对于补码来说,符号位只是判断一个数是正是负的一种方法,其实符号位也是负数进行运算的一位,补码的意思在于用模加上负数,用实际上没有符号位的数代表负数,例如模12,从12点拨到9点,往回拨3(-3),就相当于顺拨9,对于int型,32位,模就是2^8,那么-3的补码就是2^8-3,(取反加1即将2^8拆开,0xffffffff-3+1,0xffffffff-3就是取反)
- 对负数的补码的任何操作都是直接的,而不是会转化成原码再操作,而且都是有效的(除了会溢出的情况),比如-3+2,即2 ^ 8 - 3 + 2 = 2 ^ 8 - 1,又如-3 + 4,即2 ^ 8 - 3 + 4 = 1(最多保存32位,模掉2^8,结果依然是对的,补码的意义就在这,将减法转化为加法),左移右移同理
B.溢出
- 定义:计算机的数据类型的位数是一定的,只能表示确定的位数n,一旦两个同号数相加或异号数相减,计算结果有可能超出n位数可表示的范围,超出可表示范围叫溢出,那么结果>2^(n-1)-1,或<2^(n-1),就会出现溢出
- 此外由于规定最高位为符号位,溢出的结果可能是正数相加变成了负数,负数相加变成了正数
- 而同号数相减或异号数相加时,这种情况舍弃最高位并不影响运算结果,舍去最高位相当于取模操作,得到的是正确的结果,比如3中的-3+4
C.最低最高有效位
以一个二进制位为单位
LSB: least significant bit,即权值最低的一位
MSB: most significant bit,即权值最高的一位,或者说符号位
以字节为一个排列单位(1 byte = 8 bit)
LSB: least significant byte,权值最低的8位
MSB: most significant byte,权值最高的8位
D.左移右移
正数左移,左边补0,负数左移左边补1,右移,正负数都是补0,是为了保证数据的正确性;而unsigned型,因为没有符号位,所以左移右移都是补0
- 实验操作
I.位操作
1.lsbzero(将x的LSB清零)(以二进制为单位的lsb)
int lsbZero(int x) {
//method 1
/* x = x >> 1;
x = x << 1;*/
//method 2
//x = x & ~1;
//method 3
x = ~ x ;
x = x | 1;
x = ~ x;
return x;
}
[方法1]
将x右移一位,再左移一位
[方法2]
将x与最低位为0,其余位全为1的数按位与
[方法3]
将x取反,最低位与0x1按位或,再对x取反
- byteNot
将x的第n个字节取反(字节从LSB开始到MSB依次编号为0-3)
(以字节为单位的lsb、msb)
int byteNot(int x, int n) {
n<<=3; //n*8
x=x^(0xff<<n);
return x;
}
1^0=1,1^1=0,即用1^x可实现取反操作
【方法1】
为实现对字节取反,为使左移位数为8的倍数,将n<<3,实现n*8,因1^1=0,1^0=1,0xff<<n再与x异或,就可实现对x第n个字节取反
【方法2】
提取~x中第n个字节的部分存到add,再将原来的x第n个字节部分全部置为0,再加上add
- byteXor
比较x和y的第n个字节(字节从LSB开始到MSB依次编号为0-3),若不同,则返回1;若相同,则返回0
int byteXor(int x, int y, int n) {
n<<=3;
/*method 1
x>>=n;x<<=24;
y>>=n;y<<=24;
//method 2
x=(x>>n)&0xff;
y=(y>>n)&0xff;*/
//method 3
x&=(0xff<<n);
y&=(0xff<<n);
return !!(x^y);
}
思路:将n*8,然后将x、y除第n个字节外清0,再将x、y按位异或,1^x,将x取反,0^x,x不变
【方法1】
将x,y分别把第n个字节移到最右端,在移到最左端,其右端全为0,二者异或,相同则结果为0,否则不为0,用!符号将结果转化为1/0
【方法2】
同方法1,只是将x非比较字节清0方法不同,将x第n个字节移到最右端,用x&0xff的方法将其左端清0
【方法3】
将x非比较字节清0方法不同,不移动x、y,将0xff移到第n个字节,在按位与,即可实现清0
- logicalAnd(x&& y)
int logicalAnd(int x,int y){
return !!x & !!y;
【方法1】
用!!将x、y转化,非0转为1,0转为0,再按位与即可
- logicalOr(x|| y)
int logicalOr(int x, int y)
{
// method 1
return !!x | !!y;
// method 2
return !!(x|y);//同0为0,否则按位或后不为0
}
【方法1】
用!!将x、y转化,非0转为1,0转为0,再按位或
【方法2】
x|y,x和y都是0,x|y为0,若有一个非0,则x|y结果不是0,与x||y结果一致
- rotateLeft(将x循环左移n位)
int rotateLeft(int x, int n) {
int a=x;
x<<=n;
// a>>=(32-n);
>=32+(~n+1);
a&=~(~0<<n);
return x+a;
}
提示:32 - n = 32 + ( ~ n + 1) = 0xff ^ n + 1
对一个数取补码就得到他的相反数,32-n就是0xff-n+1,0xff-n就是将n取反
思路:a = x,将x左移n位,将a右移32-n位,将a除低n位外清0,再返回x+a即可
【方法1】
a=x,x左移n位,将a右移32-n位,左将并端清0,x+a即为所求
【方法2】
对于32-n有两种做法,一是32+n的补码,二是31-n+1,31-n也就是0x1f^n
- parityCheck(若x有奇数个1,则返回1;否则,返回0)
int parityCheck(int x) {
/*
method 1
x^=x<<16;
x^=x<<8;
x^=x<<4;
x^=x<<2;
x^=x<<1;
x>>=31;
}
思路1:将x分成两半,将低位的一半和高位的一半异或,异或结果相同为0,不同传为1,再将高位的一半再分成两半,再相异或,直至异或的结果传到最高位上,这时最高位上即为所有偶数个1消掉后的结果;或者反方向传到最低位上
思路2:将x两个两个分,将异或结果传到偶数位上,舍弃奇数位,假设将偶数位的数当成新的数组,并再次将异或结果累加到偶数位上,重复操作,直至异或结果传到最高位;也可反方向传到最低位
int parityCheck(int x) {
//method 2
x^=x<<1;
x^=x<<2;
x^=x<<4;
x^=x<<8;
x^=x<<16;
x>>=31;
}
II.补码运算
8.mul2OK(计算2*x,如果不溢出,则返回1,否则,返回0)
int mul2OK(int x) {
//method 1
x^=(x<<1);
x>>=31;
//x=0x11111111 or x=0x00000000
x+=1;
/* method 2
x=( (x>>31) & 1 ) ^ ( (x>>30) & 1) ^ 1;*/
return x;
}
思路:
32位数可表示的范围是-2^31~2^31-1
将x*2即将x<<1,看x<<1后是否溢出,只需看x二进制前两位
若前两位为11,x >= -(2^30),x*2>= -(2^31),在范围内;
若前两位为00,x <= 2 ^ 30 - 1 ,x*2<= 2^31 - 2,在范围内;
若前两位为10,则x<= - (2^30+1)(0xbffffff),x*2 <= - (2^31+2),超出范围;
若前两位为01,则x>=2^30,x*2>=2^31,超出范围
注:对于负数的补码左移一位,不溢出情况下即乘2的证明
Eg.-3<0,则-3补码为(2^32-3),左移一位即(2^32-3-2^31)*2=2^32-6
【实现方法1】
将前两位异或,结果移到最右端,根据逻辑移位规则,若移位后x=0x11111111,即为溢出,溢出返回0,因x+1=0,同时x=0x0时,说明未溢出,需返回1,x+1=1,故考虑到两种情况,均返回x+1即可
【方法二】
同理,判断前两位是否为10或01,将x的最高位和次高位分别移到最右端,左边清0后,将这两位异或,溢出该位为1,否则为0,为实现溢出返回0,否则返回1,将结果用1^x,取反
- mult3div2(计算(x*3)/2,朝零方向取整)
int mult3div2(int x) {
x+=(x<<1);
int lsb=x&1;
int msb=(x>>31)&1;
int y=(x>>1)+(msb & lsb);
return y;
//method 2
x+=(x<<1);
x+=1&(x>>31);
x>>=1;
return x;
}
思路:x*3用x+=(x<<1)来表示,不考虑溢出的情况,但要考虑朝零方向取整,也就是除2后的数要舍去余数,-5/2=-2,5/2=2,而对于>>1,不一定是除2,因为>>1是舍掉了lsb(最低有效位),然后将剩下的除2,对于奇数,相当于减1后除2,而对于正数是没问题的,但对于负数,如:-3(2^32-3),就是(2^32-4)/2+2^31=2^32-2,就是说-3>>1=-2,显然不符合要求,故需对负奇数>>1后的结果加1
【实现方法1】
当最高最低位都为1时,为负奇数,将除2的结果加1
【方法2】
获取x*3后的最高位,如果x是负数,则加最高位即加0x1,负奇数将将变为比他大1的负偶数,-5+1= - 4,-4/2=-2,实现向上取整,对负偶数无影响;如果x是正数,加最高位即加0
- subOK(计算x –y,如果不溢出,则返回1,否则,返回0)
int subOK(int x, int y) {
int a=(x>>31)&1;
int b=(y>>31)&1;
y=~y+1;
int c=((x+y)>>31)&1;
return !((a^b)&(a^c));
}
思路:讨论两个数相减溢出的情况,两数同号绝不会溢出,两数异号可能溢出,溢出的情况就是正负数超出表示范围,两个异号数相减就是2个正数相加或2个负数相加,当x与x-y符号不同时,即发生溢出,也就是数据影响到了符号位,而且这里并不是舍弃最高位得到正确结果的情况,参照知识储备
思路2:
根据溢出标志OF=Cout^Cn-1,提取Cn-1,再提取x和y的最高位,将三者相加,进位即Cout,
OF=1就溢出,溢出返回0,用!转化即可;同时对于y=0x80000000时,因其补码还是其本身,但一个非负数减0x80000000,结果会溢出,但OF=0,OF判断不溢出,一个负数减0x80000000,结果不会溢出,OF=1,OF判断溢出,故考虑y=0x80000000的情况,对这种情况的溢出判断取反即可
// method 2
int subOK(int x,int y){
y=~y+1;
int cn=x&0x7fffffff;
cn+=(y&0x7fffffff);
cn>>=31;
cn&=1;
int a=1&(x>>31);
int b=1&(y>>31);
int c=!(cn^((a+b+cn)>>1));
int d=!(0x80000000^y);
return c^d;
}
10.absVal(求x的绝对值)
int absVal(int x) {
/*method 1
int a=x>>31;
x=(a^x)+(1&a);*/
// method 2
int a=x>>31;
x=(a&(~x+1)) + (~a&x);
return x;
}
思路:求x绝对值,对负数取绝对值即可(对负数取反加1)
Method 1:利用x的符号位和异或的性质,a=x>>31,
对负数,0xffffffff^x=~x,1&0xffffffff&1=1;
对正数,0x00000000^x=x,0x00000000&1=0,实现同样的操作因a的不同,操作不同;
Method 2:分类讨论,用0xffffffff或0x00000000,实现是否需要取反
III.浮点数操作
11.float_abs(返回浮点数‘|f|’的二进制表示,当输入参数是NaN时,返回NaN)
unsigned float_abs(unsigned uf) {
int m=uf&0x7fffffff;
/*method 1
if(m>0x7f800000)
return uf;
*/
//method 2
if(! ((m>>23) ^ 0xff) && (uf&0x007fffff) )
return uf;
else
return m;
}
注:需要返回的是unsigned,意思是只需要把符号位变一下就行了,输入参数为NaN(全1阶码,非0尾数时)返回NaN
思路:将浮点数符号位置为0,判断阶码是否为NaN;另外unsigned型左移补0,故直接判断阶码是否全1时要注意方法
float_f2i(返回浮点数‘f’的强制整型转换“(int)f”表示)
int float_f2i(unsigned uf) {
int j=(uf>>23)&0xff;
int w=uf&0x007fffff;
int i=w+0x00800000;
if(j>158) return 0x80000000u;
if(j<127) return 0;
else{
if((uf>>31)&1){
if(j<150) return ~(i>>(150-j))+1;
else return ~(i<<(j-150))+1;
}
else{
if(j<150) return i>>(150-j);
else return i<<(j-150);
}
}
}
思路:看起来复杂,其实很简单,就是2次分类讨论
1、考虑超出范围的情况
首先因为浮点数表示范围比int型大,所以对于超出int范围的要返回0x80000000u;
取阶码j,j-127>30时,因为int型是32位,而且需要有符号位,而浮点数尾数前隐藏了一位,所以阶码-127后最大是30,即尾数*2^30,才能正确表达int型;
2、考虑0.xxxxx的情况
而对于j-127<0,就会导致float是0.xxxxx,转变为int型后是0
3、Else (127<=j<=157)
int i=尾数前加个1
当j - 127<23,i右移,j-127>23,i左移
- 考虑如果i为负数将i取反加1
下面是实验报告要求写的问题讨论,有些意思,就附在下面了
问题讨论:
1.对二进制进行位操作,而不允许使用各种整型/无符号整型操作,是难点所在,因为这意味着不能直接采用乘号、除号和减号,if-else语句,也不能通过加负号的方式对一个数去相反数,而这也正是位操作有意思的地方,因为位操作可以完美实现上述功能
2.通过二进制的位操作,我逐渐意识到,用补码表示二进制数的含义和优点,采用补码,可将负数-m用一个2^n-m的二进制数表示,这样就将减法转化为加法的形式,而在结果的真值在-2^n~2^n-1的范围内时,其结果都是正确的,尽管可能会有舍掉最高位的情况,但正数加负数,结果可能舍掉最高一位1,即减去2^n,但结果为正确值;对一个数取补码,即可得到它的相反数,这是二进制的约定
3.符号位是衔接各种操作的纽带,通过符号位的变化即可判断两数相加是否存在溢出,还可通过负数的符号位是1,正数的符号位是0,与其他操作结合实现对一个数取绝对值等操作
4.使用二进制位操作,也要考虑到二进制所存在的一些缺点,比如可能存在溢出现象,这可能会导致计算机在计算过程中因为数据范围问题,导致计算出现错误;除此外,右移运算符,还会使最低有效位被丢掉