前言

本篇博客将会剖析 CSAPP - DataLab 各个习题的解题过程,加深对 int、unsigned、float 这几种数据类型的计算机表示方式的理解。

DataLab 中包含下表所示的 12 个习题,其中 9 个和整数有关,3个和单精度浮点数有关。

函数名 功能描述 分数 操作符
bitXor(x, y) 使用 & 和 ~ 实现异或操作 1 14
tmin() 补码的最小值 1 14
isTmax(x) x 是否为补码的最大值 1 10
allOddBits(x) x 的奇数位是否全为 1 2 12
negate(x) 不使用 - 计算 x 的相反数 2 5
isAsciDigit(x) x 是否在 [0x30, 0x39] 区间内 3 15
conditional 实现条件运算符,x ? y : z 3 16
isLessOrEqual(x, y) x 是否小于等于 y 3 24
logicalNeg(x) 不使用 ! 计算逻辑非 4 12
howManyBits(x) 表示 x 的最少补码位数 4 90
floatScale2(uf) 计算无符号数 uf 所表示的浮点数的 2 倍值 4 30
floatFloat2Int(uf) 将无符号数 uf 所表示的浮点数转为整数 4 30
floatPower2(x) 计算 \(2^x\) 4 30

解题

整数题目

整数题目对代码的要求比较严格,不允许使用超过 0xFF 的整数字面量,也不能使用 if、while 等关键字,只能使用最基本的加法和位操作实现所需功能。

bitXor(x, y)

题目要求只使用 ~ 和 & 实现异或,我们只需用德摩根定律对异或的布尔表达式做一下变换即可:

\[x\oplus y = \bar{x}y+x\bar{y} = \overline{\overline{\bar{x}y} \cdot \overline{x\bar{y}} }
\]

有了上面的式子之后就很简单了,代码如下:

  1. /*
  2. * bitXor - x^y using only ~ and &
  3. * Example: bitXor(4, 5) = 1
  4. * Legal ops: ~ &
  5. * Max ops: 14
  6. * Rating: 1
  7. */
  8. int bitXor(int x, int y) {
  9. return ~(~(~x & y) & ~(x & ~y));
  10. }

tmin()

对于 4 个字节的有符号数,\(T_{min}=-2^{32-1}=0b10\cdots0\),只需将 1 左移 31 位即可得到。

  1. /*
  2. * tmin - return minimum two's complement integer
  3. * Legal ops: ! ~ & ^ | + << >>
  4. * Max ops: 4
  5. * Rating: 1
  6. */
  7. int tmin(void) {
  8. return 1 << 31;
  9. }

isTmax(x)

对于 4 个字节的有符号数,\(T_{max}=2^{32-1}-1=0b01\cdots1\),题目不允许使用移位操作,所以有必要利用一下 \(T_{max}\) 的性质来解题:

\[T_{max}=0b01\cdots1=\sim 0b10\dots0=\sim T_{min}\\
-T_{min}=\sim T_{min}+1=T_{min}
\]

也就是说,如果 x 是 \(T_{max}\) ,只需对它按位取反,再判断它满不满足相反数即自身这个性质即可。但是除了 \(T_{min}\) 之外,0(\(\sim-1=\sim0b1\cdots1=0\)) 也满足相反数即自身这一特点,所以需要将其排除。代码如下:

  1. /*
  2. * isTmax - returns 1 if x is the maximum, two's complement number,
  3. * and 0 otherwise
  4. * Legal ops: ! ~ & ^ | +
  5. * Max ops: 10
  6. * Rating: 1
  7. */
  8. int isTmax(int x) {
  9. int y = ~x;
  10. int y_ = ~y + 1;
  11. int isZero = !(y ^ 0);
  12. return !isZero & !(y ^ y_);
  13. }

allOddBits(x)

对于所有奇数位都是 1 的整数,一定满足下式:

\[x = 0b1x_{30}1x_{28}\cdots1x_{0}, 其中 x_{2i}\in \{0, 1\}\\
x\ |\ (x \gg 1)=0b11\cdots 1
\]

将 x 按位或右移 1 位的 x 一定可以得到每位都是 1 的整数,也就是 -1。但是有一个例外,当 x 为 0b1101(这里自取 4 位,方便理解)时,虽然他没有满足所有奇数位都是 1 的要求,但是仍然有 \(x\ |\ x\gg1=1101 | 1110=1111\),所以我们有必要将 x 中的 4 的整数倍位清 0,即 \(x_{4i}=0\),由于这些都是偶数位,所以不必有任何的顾虑。

只需将 \(x\& 0xEEEEEEEE\) 就能做到上述的清零操作,整数实验不允许使用大于 255 即 0xFF 的字面量,所以我们只能通过移位来构造 0xEEEEEEEE,代码如下:

  1. /*
  2. * allOddBits - return 1 if all odd-numbered bits in word set to 1
  3. * where bits are numbered from 0 (least significant) to 31 (most significant)
  4. * Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
  5. * Legal ops: ! ~ & ^ | + << >>
  6. * Max ops: 12
  7. * Rating: 2
  8. */
  9. int allOddBits(int x) {
  10. int mask = 0xEE + (0xEE << 8);
  11. mask = mask + (mask << 16);
  12. int y = x & mask;
  13. int z = y | (y >> 1);
  14. return !(~z ^ 0);
  15. }

negate(x)

要计算相反数,只需按位取反之后再加 1 即可。

  1. /*
  2. * negate - return -x
  3. * Example: negate(1) = -1.
  4. * Legal ops: ! ~ & ^ | + << >>
  5. * Max ops: 5
  6. * Rating: 2
  7. */
  8. int negate(int x) {
  9. return ~x + 1;
  10. }

isAsciiDigit(x)

这题要判断 x 是否为 Ascii 码 0~9 中的某一个,即要求 \(0x30\le x \le 0x39\),可以分两步实现判断。

首先判断低 4 位 \(x_3x_2x_1x_0\) 是否在 0~9 范围内。当 \(x_3\) 为 0 时,低 4 位在 0~7 范围内;当 \(x_3\) 为 1 时,只要 \(x_2\) 和 \(x_1\) 为 0,低四位就在 8~9 范围内。由此得到的布尔表达式为:

\[A=\bar{x}_3+x_3\bar{x}_2 \bar{x}_1
\]

接着判断 \(x_7x_6x_5x_4\) 是否为 3,只要将 x 右移 4 位之后异或 3 再逻辑取反就能得到判断结果。

  1. /*
  2. * isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0'
  3. * to '9') Example: isAsciiDigit(0x35) = 1. isAsciiDigit(0x3a) = 0.
  4. * isAsciiDigit(0x05) = 0.
  5. * Legal ops: ! ~ & ^ | + << >>
  6. * Max ops: 15
  7. * Rating: 3
  8. */
  9. int isAsciiDigit(int x) {
  10. // 判断低 4 位是否在 0~9 范围内
  11. int is0To9 = !((x & 8) ^ 0) + !((x & 14) ^ 8);
  12. // 判断高 4 位是否为 3
  13. int isThree = !((x >> 4) ^ 3);
  14. return is0To9 & isThree;
  15. }

conditional(x, y)

要实现 w = x : y ? z,只需实现函数 \(f(x, y, z)=z\ \&\ g(x)+y\ \& \ \sim g(x)\),其中 \(g(x)\) 满足下式:

\[g(x)=\left\{
\begin{aligned}
0b11\cdots 1 \quad & x=0 \\
0b00\cdots 0 \quad & x\neq 0
\end{aligned}
\right.
\]

要实现 \(g(x)\),只需先将 x 异或 0,如果 x 为 0,结果就是 0,否则为非 0 数,接着再逻辑取反,得到的数不是 1 就是 0,再按位取反并加 1,就能得到 \(g(x)\)。代码如下:

  1. /*
  2. * conditional - same as x ? y : z
  3. * Example: conditional(2,4,5) = 4
  4. * Legal ops: ! ~ & ^ | + << >>
  5. * Max ops: 16
  6. * Rating: 3
  7. */
  8. int conditional(int x, int y, int z) {
  9. int mask = ~(!(x ^ 0)) + 1;
  10. return (y & ~mask) + (z & mask);
  11. }

isLessOrEqual(x, y)

比较两个数的大小,首先应该比较符号位。如果 x 为正,y 为负,直接返回 0;如果如果 x 为负,y 为正,直接返回 1。

如果 x 和 y 同号,则判断 \(z=x-y\le0\) 是否成立。由于题目不允许使用减号操作符,所以换成判断 \(z=x+(-y)=x+(\sim y+1)\le 0\)。只要 z 的符号位为 1,x 就小于 y,如果 z 为 0,说明 x 等于 y。

  1. /*
  2. * isLessOrEqual - if x <= y then return 1, else return 0
  3. * Example: isLessOrEqual(4,5) = 1.
  4. * Legal ops: ! ~ & ^ | + << >>
  5. * Max ops: 24
  6. * Rating: 3
  7. */
  8. int isLessOrEqual(int x, int y) {
  9. // 获取符号位
  10. int signX = (x >> 31) & 1;
  11. int signY = (y >> 31) & 1;
  12. // 大小比较
  13. int z = x + (~y + 1);
  14. int isLe = !((z & (1 << 31)) ^ (1 << 31)) | !(z ^ 0);
  15. return (!(~signX & signY)) & ((signX & ~signY) | isLe);
  16. }

logicalNeg(x)

逻辑取反,x 非 0 返回 0,x 为 0 返回 1。在实现 isTmax(x) 时,我们说过 0 满足 \(-0=0\),即 \(0\ |\ (\sim0+1)\) 得到的结果还是 0。而其他非 0 数按位或自己的相反数,符号位一定会是 1。由此可以写出逻辑非的代码:

  1. /*
  2. * logicalNeg - implement the ! operator, using all of
  3. * the legal operators except !
  4. * Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
  5. * Legal ops: ~ & ^ | + << >>
  6. * Max ops: 12
  7. * Rating: 4
  8. */
  9. int logicalNeg(int x) {
  10. return ((x | (~x + 1)) >> 31) + 1;
  11. }

howManyBits(x)

题目要求计算出表示 x 的最少补码位数,比如:

  • \(0=0b0\),只需 1 位即可表示
  • \(-1=0b1\),也只需 1 位来表示
  • \(1 = 0b01 \in[-2, 1]\),需要 2 位来表示
  • \(-2=0b10\in [-2, 1]\),需要 2 位来表示
  • \(2=0b010\in [-4, 3]\),需要 3 位来表示
  • \(3=0b011\in [-4, 3]\),需要 3 位来表示
  • \(-3=0b101\in[-4, 3]\),需要 3 位来表示

观察上面的二进制数和他们所需的位数,可以发现如果 x 为正数,从左到右扫描,第一个 1 出现的位置 +1 就是所需位数。如果 x 为负数,将其按位取反转换为正数后再进行相同判断即可。

我们可以采用二分法来从左到右寻找第一个 1 出现的位置。首先去高 16 位看看有没有 1 出现,如果有就把 x 右移 16 位后的值赋给 x,再去移位后 x 的低 16 位二分查找。如果高 16 位没有出现 1,就在低 16 位二分查找。

  1. int howManyBits(int x) {
  2. int sign = x >> 31;
  3. // 将 x 转换为正数,这样只要判断最高位 1 出现的位置即可
  4. x = (sign & ~x) | (~sign & x);
  5. // 判断高16位是否存在 1,如果有就右移 x
  6. int b16 = (!!(x >> 16)) << 4;
  7. x = x >> b16;
  8. // 判断高 8 位是否存在 1,如果有就右移 x
  9. int b8 = (!!(x >> 8)) << 3;
  10. x = x >> b8;
  11. // 判断高 4 位是否存在 1,如果有就右移 x
  12. int b4 = (!!(x >> 4)) << 2;
  13. x = x >> b4;
  14. // 判断高 2 位是否存在 1,如果有就右移 x
  15. int b2 = (!!(x >> 2)) << 1;
  16. x = x >> b2;
  17. int b1 = !!(x >> 1);
  18. int b0 = x >> b1;
  19. return b16 + b8 + b4 + b2 + b1 + b0 + 1;
  20. }

浮点数题目

浮点数题目对代码的要求没有整数题目那么严格,可以在代码里面使用超过 0xFF 的整数字面量,可以使用 if、while 关键词,还能使用 ==、>= 等逻辑运算符。

floatScale2(uf)

题目要求将无符号数 uf 表示的单精度浮点数 f 乘以 2,可以分为两种情况:

  • 如果 f 为非规格化数,即 exp 字段为 0,此时 f 小于 1,只需将 uf 算术左移即可
  • 如果 f 为规格化数,即 exp 字段不为 0,乘以 2 只需将 exp+1 即可,但是 +1 之后可能使得 exp 变为 0xFF,即发生了溢出,这时候需要返回 \(+\infty\) 或者 \(-\infty\)

代码如下所示:

  1. /*
  2. * floatScale2 - Return bit-level equivalent of expression 2*f for
  3. * floating point argument f.
  4. * Both the argument and result are passed as unsigned int's, but
  5. * they are to be interpreted as the bit-level representation of
  6. * single-precision floating point values.
  7. * When argument is NaN, return argument
  8. * Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
  9. * Max ops: 30
  10. * Rating: 4
  11. */
  12. unsigned floatScale2(unsigned uf) {
  13. // 取出阶码
  14. unsigned exp = (uf & 0x7f800000) >> 23;
  15. if (exp == 255) {
  16. return uf;
  17. }
  18. // 取出符号位
  19. unsigned sign = uf & 0x80000000;
  20. // 非规格化数,直接左移扩大两倍
  21. if (exp == 0) {
  22. return uf << 1 | sign;
  23. }
  24. // 溢出
  25. if (++exp == 255) {
  26. return sign | 0x7f800000;
  27. }
  28. return exp << 23 | (uf & 0x807fffff);
  29. }

floatFloat2Int(uf)

题目要求将浮点数 f 强转为整数,根据 \(E=exp-Bias\) 的值可以分为几种情况:

  • 如果 \(E\) 小于 0,说明 f 要么是非规格化数(\(exp\) 为 0,这里没有使用 \(1-Bias\) 因为只看 \(E\) 的符号),要么是一个小于 2 的数乘上了 \(1/2^n\) ,两种情况下 f 的绝对值都小于 1,只需返回 0 即可
  • 如果 \(E\) 大于 31,说明 \(|\pm 1.XX\cdots X|\) 至少变成原来的 \(2^{32}\) 倍,由于整数只有 4 个字节,这时候发生了溢出,返回 0x80000000
  • 如果 \(23\lt E\lt 31\),说明 \(|\pm 1XX\cdots X|\) (注意这里没有小数点,所以需要大于 23)需要左移(扩大)才能表示浮点数的值 ,左移的过程中可能改变符号为负,说明发生了溢出,需要返回 0x80000000
  • 如果 \(0\le E\le 23\),说明 \(|\pm 1XX\cdots X|\) 需要右移(缩小)才能表示浮点数的值

代码如下所示:

  1. /*
  2. * floatFloat2Int - Return bit-level equivalent of expression (int) f
  3. * for floating point argument f.
  4. * Argument is passed as unsigned int, but
  5. * it is to be interpreted as the bit-level representation of a
  6. * single-precision floating point value.
  7. * Anything out of range (including NaN and infinity) should return
  8. * 0x80000000u.
  9. * Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
  10. * Max ops: 30
  11. * Rating: 4
  12. */
  13. int floatFloat2Int(unsigned uf) {
  14. // 计算阶码
  15. unsigned exp = (uf & 0x7f800000) >> 23;
  16. int e = exp - 127;
  17. // 0或小数直接返回 0
  18. if (e < 0) {
  19. return 0;
  20. }
  21. // NaN 或者 无穷大
  22. if (e > 31) {
  23. return 0x80000000;
  24. }
  25. // 尾数
  26. int frac = (uf & 0x7fffff) | 0x800000;
  27. // 移动小数点
  28. if (e > 23) {
  29. frac <<= (e - 23);
  30. } else {
  31. frac >>= (23 - e);
  32. }
  33. // 符号位不变
  34. if (!((uf >> 31) ^ (frac >> 31))) {
  35. return frac;
  36. }
  37. // 符号位变化,且当前符号为负,说明溢出
  38. if (frac >> 31) {
  39. return 0x80000000;
  40. }
  41. // 符号变化,返回补码
  42. return ~frac + 1;
  43. }

floatPower2(x)

这题比较简单,要求计算 \(2^x\) ,只要将 exp 加上 x 即可。因为 x 变化范围太大,可能导致 exp 小于 0 或者大于 255,这时候就要返回 0 或者无穷大。

  1. /*
  2. * floatPower2 - Return bit-level equivalent of the expression 2.0^x
  3. * (2.0 raised to the power x) for any 32-bit integer x.
  4. *
  5. * The unsigned value that is returned should have the identical bit
  6. * representation as the single-precision floating-point number 2.0^x.
  7. * If the result is too small to be represented as a denorm, return
  8. * 0. If too large, return +INF.
  9. *
  10. * Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
  11. * Max ops: 30
  12. * Rating: 4
  13. */
  14. unsigned floatPower2(int x) {
  15. int exp = 127 + x;
  16. // 溢出
  17. if (exp >= 255) {
  18. return 0x7f800000u;
  19. }
  20. // 太小以至于无法用非规格化数来表示
  21. if (exp < 0) {
  22. return 0;
  23. }
  24. return exp << 23;
  25. }

总结

做完习题之后收获还是挺大的,做题的过程也产生了一些想法:

  • 看书还是挺无聊的,配合 B 站的网课食用更香,而且看 CMU 网课的感觉和看国内慕课的感觉完全不一样,看慕课的时候只想着开倍数刷完了事,而看 CMU 网课的时候就觉得大牛慢慢悠悠的节奏很舒服,可以看得很投入
  • int 和 unsigned 的底层二进制数是一样的,只是看待这个二进制数的方式不同,只要记住数轴即可

以上~~

CSAPP 之 DataLab 详解的更多相关文章

  1. CSAPP 之 BombLab 详解

    前言 本篇博客将会展示 CSAPP 之 BombLab 的拆弹过程,粉碎 Dr.Evil 的邪恶阴谋.Dr.Evil 的替身,杀手皇后,总共设置了 6 个炸弹,每个炸弹对应一串字符串,如果字符串错误, ...

  2. CSAPP 之 AttackLab 详解

    前言 本篇博客将会介绍 CSAPP 之 AttackLab 的攻击过程,利用缓冲区溢出错误进行代码注入攻击和 ROP 攻击.实验提供了以下几个文件,其中 ctarget 可执行文件用来进行代码注入攻击 ...

  3. CSAPP 之 CacheLab 详解

    前言 本篇博客将会介绍 CSAPP 之 CacheLab 的解题过程,分为 Part A 和 Part B 两个部分,其中 Part A 要求使用代码模拟一个高速缓存存储器,Part B 要求优化矩阵 ...

  4. CSAPP 之 ShellLab 详解

    前言 本篇博客将会详细介绍 CSAPP 之 ShellLab 的完成过程,实现一个简易(lou)的 shell.tsh 拥有以下功能: 可以执行外部程序 支持四个内建命令,名称和功能为: quit:退 ...

  5. 《TCP/IP 详解 卷1:协议》第 10 章:用户数据报协议

    引言 UDP 稍微扩展了IP协议,使得包可以在进程间传送,而不仅仅是在主机件.--<CSAPP> IP 数据报是指 IP 层端到端的传输单元.分组(packet)是 IP 层和链路层的传输 ...

  6. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  7. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  8. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  9. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

随机推荐

  1. MySQL_fetch_array 和 MySQL_fetch_object 的区别是 什么?

    以下是 MySQL_fetch_array 和 MySQL_fetch_object 的区别: MySQL_fetch_array() – 将结果行作为关联数组或来自数据库的常规数组返回. MySQL ...

  2. 有哪些类型的通知Advice?

    Before - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置. After Returning - 这些类型的 Advice 在连接点方法 ...

  3. 【动态规划】洛谷P1802 5 倍经验日(01背包问题)

    一个洛谷普及-的题目,也是我刚刚入门学习动态规划的练习题. 下面发一下我的思路和代码题解: 我的思路及伪代码: 我的AC图: 接下来上代码: 1 //动态规划 洛谷P1802 五倍经验日 2 #inc ...

  4. 2. 使用Github

    2. 使用Github 2.1 目的 借助github托管项目代码 2.2 基本概念 仓库(Repository) 仓库用来存放项目代码,每个项目对应一个仓库,多个开源项目则有多个仓库 收藏(Star ...

  5. '\ddd'转义字符与八进制转换

    所有的ASCII码都可以用"\"加数字(一般是8进制数字)来表示.而C中定义了一些字母前加"\"来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等, ...

  6. (Math.round(num*100)/100).toFixed(2); 将输入的数字变成保留两位小数

    <input type="number" @input="onInputPrice" @blur="onPrice" data-id= ...

  7. anijs 一个小巧的动画库

    很多时候我意识到前端已近变成写h5宣传页面 我不知道是可悲 还是生活的必然 小问题 使用css animation和js animation api制作动画是目前比较流行的做法 但是最后很多人的代码就 ...

  8. 一个模仿微信群聊的H5页面

    开始 上半年小米Max发布的时候,做了一个在朋友圈传播的模仿微信的群聊界面H5页面:一群公司的大咖在群里聊小米Max,用户可以向大咖们提问,以此了解产品. 页面的主体是群聊对话,同时在对话中包含了很多 ...

  9. html5知识点补充—footer元素的使用

    使用footer元素创建脚注 顾名思义,footer元素通常位于页面的底部.尽管footer通常位于某个区域或者页面的底部,但并非总是如此.footer元素旨在包含作者.网站所有者.版权数据.网站规章 ...

  10. ES6-11学习笔记--Iterator

    迭代器 Iterator 是一种接口机制,为各种不同的数据结构提供统一访问的机制 主要供for...of消费 一句话:不支持遍历的数据结构"可遍历"   具备Symbol.iter ...