在这个 Lab 中主要需要完成 bits.c 文件中的部分函数以实现函数功能,代码必须顺序执行,即不能使用 if 条件分支或者 While 循环这一类,此外对于函数的实现必须使用指定的位操作符,例如与或非 (&,|,) 等,具体每个函数的限制也不一样。
在完成 bits.c 后使用 dlc 进行检查,例如./dlc bits.c
,检查函数的实现是否遵循指定的规则,检查通过后使用 btest 开始测试函数的输入与输出,btest 需要先编译再运行,例如make clean && make btest && ./btest
。btest 将会对每一个函数给定指定的输入,检查输出是否正确,每一个函数都有一个等级与分值,等级越高难度和分数也越高。
具体规则如下:
- 函数中禁止使用 if, do, while, for, switch 之类的控制语句。
- 文件中禁止定义任何宏,禁止定义任何额外的函数。
- 禁止调用函数。
- 在 int 类型的题目中禁止定义除 int 类型以外的变量。在 float 类型的题目中禁止定义除 int 和 unsigned 类型以外的变量,也禁止使用 flaot 类型与浮点数操作符。
- 函数中定义 int 类型的常量范围在
0x00 <= int <= 0xff
,不能大于 0xff。 - 不能使用全局变量,只能使用函数参数与局部变量。
- 操作符限制为一元操作符
! ~
和位操作符& ^ | + << >>
,部分函数中可能仅允许使用一部分操作符。禁止使用二元与三元操作符,例如|| && ? -
- 在每个函数中会限制出现的操作符的数量,一条语句可以使用多个数量限制内的操作符。
背景知识
在 C 语言中,32 位的操作系统上一个 int 类型的变量占 32 个比特位,即 4 个字节。从右向左,代表从低位到高位,其中最高位 (最左边) 的比特位表示该数字的符号,1 表示该数字为负数,0 表示该数字为正数,后续的 31 个比特位中存放才是该数字的二进制数据,能存储的数字范围为 $-2^31<=int<=2^31$,即 $-2147483648<=int<=2147483648$。与 int 相比,无符号数 unsigned int 不需要符号位,所以可以多存储一位数据,其范围为 $-4294967296<=unsigned<=4294967296$。
上述的数值二进制表示方法称为原码,除了原码,计算机中还有反码与补码两种表示方法。正数的反码与补码和原码相同,而负数的反码为:符号位不变,其余位取反,负数的补码为:负数的反码加 1。反码的意义在于进行减法的时候计算机计算比较方便,而补码的意义在于解决 +0 与 -0 的问题:由于 int 的最高位是符号位,当数据位都为 0 时,数字 0 就有了两种表示方法,符号位是 0(即 0x0)代表 +0 和符号位是 1(即 0x80000000)代表 -0。此外补码还可以得到该数的相反数。
位操作符是 C 语言中最基本的操作符,将两个数字按比特位进行操作,使用位操作符的计算速度是最快的。C 语言中主要有以下位操作符,其中移位运算符的优先级比算术运算符的优先级更低。
符号 | 名称 | 说明 |
---|---|---|
& | 位且 | 两个数按位进行且运算,两个相比较的比特位中只要有一个 0,那么该位的结果就是 0 |
| 位或 | 两个数按位进行或运算,两个相比较的比特位中只要有一个 1,那么该位的结果就是 1 | |
^ | 异或 | 两个数按位进行异或运算,两个相比较的比特位如果不一样那么该位的结果为 1,否则为 0 |
~ | 取反 | 将该数按位进行取反,0 变成 1,1 变成 0 |
<< | 算术左移 | 将该数每一位左移指定位数,右边补 0,左移几位就是将原数乘以 2 的几次方 |
>> | 算术右移 | 将该数每一位右移指定位数,左边补符号位,若符号位是 1 则补 1,负责补 0,仅针对正数而言右移几位就是将原数除以 2 的几次方 |
浮点数的表示方法与整数完全不同。IEEE 规定单精度浮点数 float 用 1 位表示符号,8 位表示指数,剩下的 23 位表示尾数,双精度浮点数用 1 位表示符号,11 位表示指数,剩下的 52 位表示尾数。在计算机中任何浮点数都可以表示为
$$(-1)^s*(1.m)^*2^(E-127)$$
其中 s 代表符号位,m 代表尾数,E 代表阶码。
- 如果阶码 E=255 并且 m 非零,则该数不是一个属,代表 NaN(Not a number)。
- 如果阶码 E=255 并且 m 为 0,那么该数根据符号位代表正负无穷。
- 如果阶码 E=0 并且 m 为 0,那么该数根据符号位代表正负零。
Bit Manipulations
bitAnd
使用 ~
与|
实现位与运算符,例如bitAnd(6, 5) = 6 & 5 =4
,最大操作符数量为 8,等级为 1。根据对合律与对偶律可得a&b = ~(~(a&b))=~(~a|~b)
1 | /* |
getByte
获取一个整数 x 中的第 n 个字节,n 的范围在0 <= n <= 3
,例如getByte(0x12345678,1) = 0x56
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 6,等级为 2。
可以利用位且操作符的性质,让其余位清 0 即可获取指定位的数值,例如0x1234 & 0x00ff= 0x34
,0x1234 & 0xff00 = 0x12
。可以让 0xff 左移 n*8 位就可以获取第 n 个字节,其余位都被清 0 了,之后再将获取到的字节右移回 n*8 位就可以了。由于无法使用乘号*
,但是乘数刚好是 2 的倍数,因此可以使用左移代替乘法n * 8 = n << 3
。原理如下所示。
1 | // x=0x12345678, n=1 |
注意 C 语言里使用的是算术右移,即有符号数右移时符号位保持不变,符号位向右移动后正数在符号位补 0,负数补 1,因此若右移了一个负数,则被右移的位数上将都为 1,因此在最后还需要使用位且运算符只保留前 1 个字节。如下所示。
1 | // x=0x80000000, n=3 |
1 | /* |
logicalShift
将整数 x 右移 n 位,n 的范围在0 <= n <= 31
,例如logicalShift(0x87654321,4) = 0x08765432
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 20,等级为 3。
在 C 语言里的位右移是算术右移,若右移的数是有符号数,则符号位正数补 0,负数补 1,例如 0x8000 右移 8 位得到 0xff80,而逻辑右移是不管符号位的,全部补 0。因此在执行算术右移以后,需要将被移动的位数全部清 0。将 0x1 左移 31 位以后就变成了 0x80000000(0b100000…),这时再使用算术右移 n-1 位就可以将前 n-1 位指定为 1,再取反就可以将前 n-1 位清 0。原理如下。
1 | 0x00000001 |
1 | int logicalShift(int x, int n) { |
bitCount
获得整数 v 中二进制 1 的个数,例如bitCount(5) = 2, bitCount(7) = 3
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 40,等级为 4。
这个题非常难,其原理就是将 32 位进行分组,每 2 位为一组,分别计算出每组 1 的数量,关于计算每组 1 的数量可以使用 010101…这样的掩码,先用位且运算计算每组低位 1 的数量,再将原数右移 1 位计算每组高位 1 的数量,相加即可得到每组 1 的数量,例如:
1 | // 假设原数为 0x7a2d,其二进制如下 |
之后再每 2 组相加得到每 4 组 1 的数量,相加可以使用掩码 00110011…,分别得到低两位 1 的数量与高两位 1 的数量,再相加,例如:
1 | // 刚才计算每两位为一组的 1 的数量 |
之后再重复这样的操作,再分为 8 位一组计算,掩码为 0b0000111100001111…,再分为 16 位一组计算,掩码为 0b0000000011111111…,最后相加即可得到 32 位里 1 的数量。
1 | /* |
bang
不使用非运算符计算出!x
,例如bang(3) = 0, bang(0) = 1
。允许的操作符为~ & ^ | + << >>
,最大操作符数量为 12,等级为 4。
非运算符的作用非常简单,就是 0 返回 1,非 0 返回 0,所以重点就需要判断输入的数是否是 0。关于判断是否为 0 有两种方法,一种是将二进制位所有高位的 1 全部右移到低位,直到只剩下 1 个二进制位为止,直接判断该二进制是否为 0 即可,如下所示:
1 | // 设原数 x 为 0x18 |
另一种方法就是利用 0 的补码的特殊性质。计算补码使用的公式为 ~x+1
,而除了 0 和 0x80000000 以外其他数使用该公式都会得到其相反数,最高位符号位总是不同。而 0 和 0x80000000 这两个数计算出来的则是他们本身:如果要计算 0 的补码,就需要计算~0+1
,而~0 的值是 0xffffffff,如果再加 1 则会溢出,变成 0x0,所以 0 的补码还是 0,最高位也是 0,而 0x80000000 的补码最高位还是 1,因此就可以通过比较x
与~x+1
的符号位是否是 1 来判断 x
是否是 0。
1 | /* |
Two’s Complement Arithmetic
tmin
返回最小的补码整数。限制的操作符为! ~ & ^ | + << >>
。最大操作符数目为 4。等级为 1。最小的补码整数为 0x80000000。
1 | /* |
fitsBits
判断一个数 x 是否可以用 n 位补码表示,其中1 <= n <= 32
。例如fitsBits(5,3) = 0, fitsBits(-4,3) = 1
。允许使用的操作符为! ~ & ^ | + << >>
。最大操作符数量为 15。等级为 2。
此题化简一下就是需要判断:
- 如果是正数且 x 的第 n 位为 0,那么就可以用 n 位补码表示。
- 如果是负数且 x 的第 n 位为 1,那么就可以用 n 位补码表示。
其原理为:如果要用 n 位的补码表示,那么所表示出来的补码第 n 位必须作为符号位,如果原码第 n 位与其符号位不相同,那么就需要 n+1 位补码才能表示。例如:
1 | // 0x5 |
1 | /* |
divpwr2
实现计算 x 除以 2 的 n 次方,其中0 <= n <= 30
,向 0 取整,例如divpwr2(15,1) = 7, divpwr2(-33,4) = -2
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 15,等级为 2。
如果是正数的话右移 n 位就是除以 2 的 n 次方,但是对负数并不适用,由于需要向 0 取整,而移位运算都是向下取整,因此就需要给负数添加一个补充值:$2^n-1$
1 | /* |
negate
返回 x 的相反数,例如negate(1) = -1
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 5,等级为 2。前面提到过,使用计算补码的公式即可得到一个数的相反数。
1 | /* |
isPositive
判断一个数是否为正数,例如 isPositive(-1) = 0
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 8,等级为 3。首先需要使用!x
判断是否为 0,之后就可以判断符号位是否为 1。
1 | /* |
isLessOrEqual
判断一个数是否小于等于另一个数,例如isLessOrEqual(4,5) = 1
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 24,等级为 3。
根据不等式的性质可以将 x <= y
转化为 0 <= y-x
,就是需要判断y-x
是不是负数,其中 -x
可以表示为~x+1
,但是要分三种情况:
- x 和 y 同号,直接做减法不会溢出。
- x 和 y 异号,可能会发生溢出。该情况下若 x 为负数,则肯定返回 1,否则肯定返回 0。
- x 和 y 相等。
1 | /* |
ilog2
返回以 2 为底的 log(x),其中x > 0
,例如ilog2(16) = 4
。允许的操作符为! ~ & ^ | + << >>
,最大操作符数量为 90,等级为 4。
此题解法来自stackoverflow
可以用公式表示为
$$log_2(x) = 16a + 8b + 4c + 2d + 1*e (a,b,c,d,e = 0 or 1)$$
依次求出 abcde 即可。
1 | /* |
Floating-Point Operations
float_neg
返回 uf 的相反数,如果 uf 是 NaN 则返回自身。允许使用条件与循环语句。允许使用任何 int 与 unsigned 操作符。最大操作符数量为 10,等级为 2。
可以直接修改符号位返回相反数。另外如果 uf 的指数位为 0xff 并且小数位非 0 时,uf 为 NaN,因此就需要判断 uf 的绝对值是否大于 0x7f800000。
1 | /* |
float_i2f
将一个整数转换为浮点数表示的形式。允许使用条件与循环语句,允许使用任何 int 和 unsigned 操作符。。最大操作符数量为 30,等级为 4。
浮点数的表示主要有三个部分:符号位 sign,指数位 exp 和小数位 frac。首先找到符号位后将 x 取绝对值 (符号位归 0),通过右移找到 x 最高位的 1 的位置(找到 x 的 2 的最高次方) 再加 127 就得到指数位 exp,之后就需要计算小数位 frac,由于 int 有 31 位,而浮点数小数位只有 23 位存放数据,因此从 int 转为 float 会丢失 8 位的精度,这时就需要对小数位向偶数舍入。int 类型中有两个特殊值:0 和 0x80000000,这两个特殊值不能通过移位计算指数位与小数位因此要单独处理。
1 | /* |
float_twice
返回浮点数 uf 乘 2 的结果,如果 uf 为 NaN 则返回自身。允许使用条件与循环语句,允许使用任何 int 和 unsigned 操作符。最大操作符数量为 30,等级为 4。浮点数乘 2 其实就是将 exp 加 1,但是需要单独处理一些特殊情况,例如 NaN,正负 0(0x0, 0x80000000)和非规格化的浮点数。
1 | /* |