目录
一、实验目的
1. 更好地熟悉和掌握计算机中整数和浮点数的二进制编码表示。
2. 加深对数据二进制编码表示的了解。
3. 使用有限类型和数量的运算操作实现一组给定功能的函数。
二、实验要求及注意事项
1. 注意给出的可以使用的运算符及最大个数。
2. 除了float类型的两个操作函数可以使用if-else条件语句外,其它函数只能用顺序结构进行代码设计和实现。
3. 本实验使用btest程序进行评分时记分为50分制,但最后教师端进行实际成绩核算时会使用100分制(乘以2)。
三、实验原理与内容
本实验每位学生拿到一个datalab-handout.tar文件。学生可以通过U盘、网盘、虚拟机共享文件等方式将其导入到Unbuntu实验环境中,选择合适位置存放。然后在Ubuntu环境下解压。解压后,根据文件中的叙述和要求更改bits.c文件,其他文件不要动。本次实验的主要操作方式为:使用C语言的位操作符实现题目要求。
完成实验后提交给老师时,将自己编写的bits.c改名为“bits_学号.c”的形式交给学委统一收集。文件名例:bits_18210320101.c。如果文件名出现错误,则自动评分程序不会给出分数,请务必注意!!!!
需要完成bits.c中下列函数功能,具体分为三大类:位操作、补码运算和浮点数操作。
1.位操作
表1列出了bits.c中一组操作和测试位组的函数。其中,“级别”栏指出各函数的难度等级(对应于该函数的实验分值),“功能”栏给出函数应实现的输出(即功能),“约束条件”栏指出你的函数实现必须满足的编码规则(具体请查看bits.c中相应函数注释),“最多操作符数量”指出你的函数实现中允许使用的操作符的最大数量。
也可参考tests.c中对应的测试函数来了解所需实现的功能,但是注意这些测试函数并不满足目标函数必须遵循的编码约束条件,只能用做关于目标函数正确行为的参考。
表1 位操作题目列表
本题分数 | 函数名 | 功能 | 约束条件 | 最多操作符数 |
1 | isZero | 判断变量x是否为0。如果为0,则返回1;否则,返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 2 |
1 | specialBits | 构建0xffca3fff,并返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 3 |
1 | upperBits | 根据输入的变量n,构建一个高n位为1其他位为0的数,并返回该值。 注:0<= n <=32 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 10 |
1 | bitMatch | 构建一个比特序列(int型),构成规则如下:如果x和y在某一个bit位置的值相同,则此序列的相应位置为1,否则该位置值为0。 | 仅可以使用以下操作符: ~ & | | 14 |
1 | bitOr | 计算按比特或(x | y),并将计算结果返回。 | 仅可以使用以下操作符: ~ & | 8 |
4 | logicalNeg | 使用位操作符实现逻辑非(!x)操作,并将取逻辑非之后的结果返回。 | 仅可以使用以下操作符: ~ & ^ | + << >> | 12 |
4 | bitParity | 如果x中包含奇数个0,则返回1;否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 20 |
2 | byteSwap | 将x的第n字节和第m字节交换, 0 <= n <= 3, 0 <= m <= 3, 然后将交换后的值返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 25 |
2 | getByte | 提取x的第n个字节。0 <= n <= 3 (0代表最最低为字节,3代表最高位字节),并将其返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 6 |
2 | oddBits | 返回一个32bit数,这数的所有第奇数个bit位置的值为1。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 8 |
3 | replaceByte | 将x的第n个字节用 c 进行替换, 0 <= n <= 3, 0 <= c <= 255 并将替换后的结果返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 10 |
3 | rotateLeft | 将x向左循环移位n个bit。循环移位是指左边移除去的比特自动填充到右边空出的位置上。 0 <= n <= 31,并将循环移位后的值返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 25 |
2.补码运算
表2列出了bits.c中一组使用整数的补码表示的函数。可参考bits.c中注释说明和tests.c中对应的测试函数了解其更多具体信息。
表2 补码运算题目列表
本题分数 | 函数名 | 功能 | 约束条件 | 最多操作符数 |
2 | negate | 将输入参数x的值取相反数,返回-x。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 5 |
4 | absVal | 计算变量x的绝对值,并将其绝对值返回。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 10 |
3 | isGreater | 如果x > y,则返回1,否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 24 |
2 | isNegative | 如果x < 0,返回1;否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 6 |
4 | isPower2 | 如果x是2的整数次幂,则返回1;否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 20 |
3 | addOK | 如果x+y没有溢出,则返回1;否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 20 |
3 | subtractionOK | 如果x-y没有溢出,则返回1;否则返回0。 | 仅可以使用以下操作符: ! ~ & ^ | + << >> | 20 |
3.浮点数操作
表3列出了bits.c中一组浮点数二进制表示的操作函数。可参考bits.c中注释说明和tests.c中对应的测试函数了解其更多具体信息。注意输入参数和返回结果均为unsigned int类型,但应作为单精度浮点数解释其32 bit二进制表示对应的值。
表3 浮点数操作题目列表
本题分数 | 函数名 | 功能 | 约束条件 | 最多操作符数 |
2 | floatAbsVal | 通过bit级操作返回一个float型浮点数的绝对值。如果输入参数为NaN,则直接返回输入参数的原值。 | 可以使用任何的操作符,包括||和&&。也可以使用if,while。 | 10 |
2 | floatIsEqual | 判断两个浮点数是否相等,如果相等则返回1,否则返回0。 如果输入参数中含有NaN,则返回0。 注:+0和-0被当作相等的情况对待。 | 可以使用任何的操作符,包括||和&&。也可以使用if,while。 | 25 |
四、实验设备与软件环境
1. Linux操作系统—64位 Ubuntu 18.04
2. C编译环境(gcc)
3. 计算机
五、实验过程与结果
1、操作符及运算概览
(1)位运算和逻辑运算
符号 | 描述 | 运算规则 |
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个为都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
! | 非 | 单目运算,将真值变为假(0),假变为真(1) |
+ | 加法 | 前后值相加 |
|| | 短路或 | 前后值全false时,计算结果为false,否则为true,且具备短路功能,即第一个操作数false则不计算第二个操作数 |
&& | 短路与 | 前后值全true时,结算结果为true,否则为false,与短路或同样具备短路功能 |
(2)补码运算
a. 机器数和真值
在学习原码,反码和补码之前,需要先了解机器数和真值的概念。一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号,正数为0,负数为1。比如,十进制中的数+3,计算机字长为8位,转换成二进制就是00000011。如果是-3,就是10000011。
那么,这里的 00000011 和 10000011 就是机器数。
因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
b. 原码、反码和补码
现在我们知道了计算机可以有三种编码方式表示一个数. 对于正数因为三种编码方式的结果都相同:
[+1] = [00000001]原 = [00000001]反 = [00000001]补
所以不需要过多解释. 但是对于负数:
[-1] = [10000001]原 = [11111110]反 = [11111111]补
可见原码, 反码和补码是完全不同的. 既然原码才是被人脑直接识别并用于计算表示方式, 为何还会有反码和补码呢?
① 计算机中符号位参与运算
首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域的加减. (真值的概念在本文最开头). 但是对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单. 计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法. 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了。于是人们开始探索将符号位参与运算, 并且只保留加法的方法。首先来看原码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的.这也就是为何计算机内部不使用原码表示一个数.
为了解决原码做减法的问题, 出现了反码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在"0"这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0。
② 补码运算
于是补码的出现, 解决了0的符号以及两个编码的问题:
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
-1-127的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是注意因为实际上是使用以前的-0的补码来表示-128, 所以-128并没有原码和反码表示.(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的)
使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127].
因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值。
(3)浮点数
图1 浮点数的表示
浮点数的表示基于IEEE 754标准,其表示如上图1所示。符号位s为0时,则表示为数值的绝对值。特殊值NaN为非数值,是指当exp全为1,也称无定义数。
2、功能实现与结论
结合实验原理与内容,对文件bits.c按照要求进行更改,完成三大类功能:位操作、补码运算和浮点数操作。基于具体功能实现要求,对文件bits.c进行如下更改,代码及详解如下所示:
(1)isZero
代码:
int isZero(int x) {
return !x;
}
解析:
判断变量是否为0,结果非1即0,考虑使用逻辑非“!”来返回结果,实现当变量为0时,返回1,否则返回0。
(2)Negate
代码:
int negate(int x) {
//计算机用补码运算
return ~x+1;
}
解析:
为了返回相反数,考虑使用补码运算,即将x取反后加1就可以得到相反数。
(3)specialBits
代码:
int specialBits(void) {
/*
f f c a 3 f f f
1111 1111 1100 1010 0011 1111 1111 1111
0000 0000 0011 0101 1100 0000 0000 0000
0000 0000 0000 0000 0000 0000 1101 0111
==128+64+16+4+2+1=215
*/
return ~(215<<14);//~(0XD7<<14)
}
解析:
基于题目要求,考虑先进行逆向拆解,首先0xffca3fff它对应的二进制数为:1111 1111 1100 1010 0011 1111 1111 1111,发现其只有中间部分较为特殊,高10位和低14位均为1,考虑使用取反求得特殊值为1101 0111(取反后),通过移位来进行运算,得到题目所求构建数。即将1101 0111(215)向左移14位,再取反就可以完成要求。
(4)upperBits
代码:
int upperBits(int n) {
return ((!!n)<<31)>>(n+(~0));
}
解析:
为了得到高n为为1其它位为0的二进制数,思路考虑为构建最高位为1然后算术右移n-1位进行构建。但考虑到n=0时,它的值直接为0,所以不能直接通过1进行移位,则使用逻辑非“!”运算:!!n—当n=0,!!n=0;否则!!n=1。由于没有减法运算,右移n-1位即可用n+(~0)来表示。
n+(~0)==n-1
(5)bitMatch
代码:
int bitMatch(int x, int y) {
return (x&y)|(~x&~y);
}
解析:
考虑到离散数学中的德摩根定律:两数相交取反等于两数取反相并。类比至位运算中,基于x和y某一bit位置相同值输出为1,否则为0,得出只有四种可能:00、01、10、11。而x&y仅能实现11的对应可能,基于德摩根定律将两数取反,即与运算上~x和~y,可以实现00的对应可能并输出为1,再将两个结果进行或运算得到要求构建的比特序列(int型)
(6)bitOr
代码:
int bitOr(int x, int y) {
return ~(~x&~y);
}
解析:
同样基于德摩根定律,对两数取反再与运算再次取反,可以得到或运算结果。
(7)absVal
代码:
int absVal(int x) {
return (x^(x>>31))+((x>>31)&1);
}
解析:
正数和零的绝对值为本身,负数的绝对值为取反加一,符合补码运算原理。使用算术右移基于最高位1/0在左边补1/0,则x>>31来区分正负数,并与原x进行异或运算,如果x为正数则保留,为负数则取反;考虑加一:负数的绝对值需要考虑加一,但正数不需要,则依靠x>>31区分并与1进行与运算(正数算术右移31位全为0,则与1与运算为0,即不加1;负数反之)
(8)logicalNeg
代码:
int logicalNeg(int x) {
return ((x|(~x+1))>>31)+1;
}
解析:
逻辑非中,基于~x+1为x的相反数,考虑使用算术右移31位来区分0和非0。对x的本身及相反数进行或运算:x=0时结果为0;x!=0时高位为1。最后+1使用溢出来区分0/1。
(9)bitParity
代码:
int bitParity(int x) {
x^=x>>16;
x^=x>>8;
x^=x>>4;
x^=x>>2;
x^=x>>1;
return x&1;
}
解析:
x中包含奇数个1和奇数个0是同等含义。将各bit上的数值进行异或运算,奇数个1时返回1,否则返回0,利用这一特点,求解各bit上数值的异或运算结果并与1进行与运算,可得题解。考虑相折,进行移位运算对高16位于低16位进行异或运算,得到的结果再分成高8位和低8位并继续异或运算直到得出最低位的数值,即为各bit位置上的数循环异或的结果。若设置8bit数值,将各bit位标号为1-8,可预览效果为:
原序列 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
>>4 | 0/1 | 0/1 | 0/1 | 0/1 | 1 | 2 | 3 | 4 |
异或 | 1^5 | 2^6 | 3^7 | 4^8 | ||||
>>2 | 1^5 | 2^6 | ||||||
异或 | 1^3^5^7 | 2^4^6^8 | ||||||
>>1 | 1^3^5^7 | |||||||
异或 | 1^3^5^7^2^4^6^8 |
(10)byteSwap
代码:
int byteSwap(int x, int n, int m) {
int a = (x>>(n<<3))&0xff;
int b = (x>>(m<<3))&0xff;
int am = a<<(m<<3);
int bn = b<<(n<<3);
int mask = x&~((0xff<<(n<<3)|0xff<<(m<<3)));
return bn+am+mask;
}
解析:
为了交换不同字节,先把n、m处的字节提取出来成a、b。n<<3=n*8,是因为1字节=8位,要定位到n字节就*8。&0xff只取低8位;再把a、b扩位到它要放入的位成am、bn;接着把x处的n、m字节重置为0,利用y+(~y+1)(补码)=0的性质,成bm、an,bm&an即x处的n、m字节重置为0。最后把它们结合起来成bn+am+(an&bm),即完成。
(11)getByte
代码:
int getByte(int x, int n) {
return (x>>(n<<3))&0xff;
}
解析:
1个字节为8个比特,所以n<<3相当于n*8,然后将x向右移那么多位,再同0xff进行与运算保留低八位的值,即可得第n个字节的值。
(12)isGreater
代码:
int isGreater(int x, int y) {
int sign = ((~x&y)>>31)&1;
int mark = ~((x^y)>>31);
int equl = !!(x^y);
return sign|((mark)&(~(x+~y+1))>>31&equl);
}
解析:
当x>y时,若x为正数,y为负数,结论则成立;所以需要考虑两种情况,一种是x为正数,y为负数时直接返回1,反之就要满足x和y同号且值不相等,并且x-y值符号为0(值为正数),同时保证x和y不相等。即:
sign检验是否x为正数,y为负数,是则返回1,否则返回0;
mark检验x和y是否异号,异号返回0,同号则返回1;
equl检验x和y是否相等,相等则返回0,否则返回1;
(13)isNegative
代码:
int isNegative(int x) {
return ((x>>31)&1);
}
解析:
为了得到负数返回1,其他返回0,将x向右移31位,符号位就位于末尾了,负数的符号位为1,非负为0,则再和1进行与运算保存最低位就可以得到了结果了。
(14)isPower2
代码:
int isPower2(int x) {
return (!(x>>31)&!(x&(x+(~0))))&!!x;
}
解析:
每个数在二进制中都是唯一的。也就是2的幂,在二进制中只有一位是1其他全是0才有可能是2的幂(除了第一位是1不是2的幂)。即可以用x&(x-1)来判断,如果只有一个1返回0,加个逻辑非“!”运算:!(x&(x+(~0));还要考虑是不是负数,是负数就返回0:!(x>>31);最后判断是不是0,是0也要返回计算:!!x。
(15)addOK
代码:
int addOK(int x, int y) {
return !((!(x>>31^y>>31))&(x>>31^(x+y)>>31));
}
解析:
发现条件可以概括为:x和y符号位相同,且x和sum符号位不同时,发生了溢出。
(16)subtractionOK
代码:
int subtractionOK(int x, int y) {
return !((x>>31^y>>31)&(x>>31^(x+~y+1)>>31));
}
解析:
不同于addOK,将x-y处理为x+~y+1即可。条件为:x和y符号位不同,且x和sum符号位不同时,发生了溢出。
(17)oddBits
代码:
int oddBits(void) {
return (0xaa)+(0xaa<<8)+(0xaa<<16)+(0xaa<<24);
}
解析:
构造0xaaaaaaaa。
(18)replaceByte
代码:
int replaceByte(int x, int n, int c) {
return (c<<(n<<3))+(x&~(0xff<<(n<<3)));
}
解析:
将数据c和0xff算术左移n*8位,并且使用构建的新0xff数与x进行与运算,顺利把x的第n个字节清空,最后加上移位后的c,得到题解。
(19)rotateLeft
代码:
int rotateLeft(int x, int n) {
return (x<<n)|((x>>(32+~n+1))&(~0+(1<<n)));
}
解析:
先得到左移n位后的数:x<<n,考虑循环被移位的数据时,即留存被循环的高位,则先构造一个高32-n位为0,低n位都为1的特殊数:~0+(1<<n);为了保留高n个bit的值,则将x向右移动32-n位,把前n位移到最低位并与前面的特殊数进行与运算,成功保留。再跟x<<n进行或运算,得到题解。
(20)floatAbsVal
代码:
unsigned floatAbsVal(unsigned uf) {
int x=uf&0x7fffffff;
if(x>0x7f800000)
return uf;
else
return x;
}
解析:
基于IEEE浮点表示法,最高位为符号位s,负数s=1,正数s=0,只要把符号位变为0,即可取绝对值,即x=uf&0x7fffffff。NaN表示的是指阶码全为1,小数域不全为0的数,经过上述符号转变后为0x7f8。即当x大于0x7f8说明x为NaN。
(21)floatIsEqual
代码:
int floatIsEqual(unsigned uf, unsigned ug) {
if(!(uf&0x7fffffff)&&!(ug&0x7fffffff))
return 1;
if((uf&0x7fffffff)>0x7f800000)
return 0;
if((ug&0x7fffffff)>0x7f800000)
return 0;
return uf==ug;
}
解析:
首先判断是否都为0,都为0(+-0)则相等,return 1;再判断uf或ug绝对值是否大于NaN,可基于floatAbsVal中来构造if。如果三个条件都不满足,再来用==判断两个数是否相等,得到题解。
3、代码检验
(1)使用dlc检查函数实现代码是否符合实验要求的编码规则。
首先./dlc bits.c直接检测是否有错误。如图2所示:
图2
由图知,如果没有任何错误输出,则表示bits.c文件的编写无误,基本符合要求。
然后用-e选项调用dlc,观察操作符数。如下图3所示。通过此选项可以查看每一道具体题目是否符合要求。如果没有任何问题,输出结果如下图1.2所示。如果局部出现问题,则输出会类似于图4所示,在具体位置表示出相应题目的具体问题。对于本例,小题1:语法错误,小题3:超过了最多操作符数量要求,原题要求不超过3个,结果使用了5个。
正在上传…重新上传取消正在上传…重新上传取消
图3 无问题情况
图4 有问题的情况
(2)使用 btest 检查函数实现代码的功能正确性。
首先使用make编译生成btest可执行程序,如图5所示。部分warning不需要特殊处理,但如果出现的warning过多则需要适当注意是否程序中有错误。
图5
然后调用 btest 命令检查 bits.c中所有函数的功能正确性。如图6所示,得分会以数字形式显示在左侧,错误显示则显示error。
图6