简介:这份资源是袁春风《计算机系统基础(第二版)》第三章课后习题的参考答案,面向正在学习计算机组成与系统底层原理的高校学生及考研复习者,帮助解决指令系统、寻址方式与机器级表示等章节练习中的疑难。压缩包内仅含1个PDF文件,约3.15MB,内容按题号逐题给出解答,涵盖寻址方式后缀判定、指令格式错误分析、数据类型转换表、函数参数栈布局、算术与逻辑运算标志位变化以及程序执行后寄存器取值等关键知识点。目前已有7731人学习下载,说明其在校内课程与备考场景中认可度较高。读者可借助逐题对照的答案与运算过程,理清OF、SF、ZF、CF等标志位的判定逻辑,掌握movsbl、movzbl等指令的机器级表示,并对照C语言代码与汇编片段理解参数传递与算术运算的底层实现,适合作为课后自测与期末复习的参考材料。
1. 计算机系统基础第三章的课后题,为什么值得你花一晚上啃透
很多人学《计算机系统基础》这类课,习惯把课后习题当成“对答案”的负担:翻到答案页,抄一遍,合上书,结束。但第三章的题目恰恰是最不该这么处理的一类——它讲的是数据的机器级表示,整数补码、无符号数回绕、浮点数 IEEE 754 编码、类型转换与截断,这些东西你在写 C 或 C++ 时踩过的每一个“玄学”bug,几乎都能在这一章的习题里找到原型。换句话说,这一章的课后题不是用来背的,是用来“跑”的:把每道题当成一段待验证的代码,用编译器、调试器和几行打印语句去核对你的手算结果,你才算真正把这一章吃进去了。这篇笔记面向两类人:正在跟这门课、被补码和浮点转换绕晕的学生,以及想回头把底层数据表示补扎实的开发者。我会按“先立概念、再动手验证、最后排坑”的顺序,把第三章最典型的几类题型拆成可复现的操作路径,而不是给你一份干巴巴的答案清单。
2. 第三章到底在考什么:从位模式到数值的三条主线
2.1 无符号与补码:同一串比特,两种读法
第三章的底层逻辑只有一句话:同一串二进制位,按不同的编码规则解释,就得到不同的数值。无符号编码把每一位都当正权重,补码编码则把最高位当成负权重。这个差别决定了后面所有题目的走向。
以 8 位为例,位模式11111111:
- 按无符号读,是 255;
- 按补码读,是 -1。
这不是文字游戏,而是硬件层面的真实行为。加法器本身不区分有无符号,它只做二进制加法,是“你怎么解释结果”决定了溢出判断和比较运算的走向。所以第三章的题目反复在训练你一件事:拿到一个位模式,先问“它被声明成什么类型”,再动手算。
补码求负的通用做法是“取反加一”,但更值得记住的是它的数学定义:对 n 位补码,位模式 x 对应的值是
value = -x[n-1] * 2^(n-1) + Σ x[i] * 2^i (i 从 0 到 n-2)手算时用这个定义比死记“取反加一”更不容易错,尤其是在位数不是 8 的题目里。
2.2 类型转换与扩展:符号扩展和零扩展的分水岭
第三章另一条主线是宽度变化时怎么补位。规则很干脆:
- 无符号数扩展:高位补 0(零扩展);
- 补码数扩展:高位补符号位(符号扩展)。
这两条规则一旦记反,题目必错。比如把short的 -1(16 位全 1)扩展成int,符号扩展后仍是 32 位全 1,值还是 -1;但如果错误地按零扩展,就变成了 65535。C 语言里“有符号和无符号混用”导致的诡异比较结果,根子就在这里。
截断则相反:从宽变窄,直接砍掉高位,保留低位。这一步不报错、不警告,是很多隐蔽 bug 的来源。
2.3 浮点数:IEEE 754 的三段式拆解
浮点部分的核心是把一个 float 拆成三段:符号位、阶码、尾数。以 32 位单精度为例:
| 字段 | 位数 | 位置 | 含义 |
|---|---|---|---|
| 符号 s | 1 | 第 31 位 | 0 正 1 负 |
| 阶码 e | 8 | 第 30–23 位 | 实际指数 = e - 127 |
| 尾数 f | 23 | 第 22–0 位 | 隐含前导 1,即 1.f |
规格化数的值是(-1)^s × 1.f × 2^(e-127)。阶码全 0 和全 1 是特殊情形:全 0 表示非规格化数和 0,全 1 表示无穷大或 NaN。第三章的浮点题基本围绕“给定位模式求十进制值”和“给定十进制值求位模式”两个方向来回考,把这张表背熟、把隐含的 1 记住,一半的题就稳了。
3. 把课后题当代码跑:手算加验证的完整流程
3.1 用一段 C 程序验证补码与无符号的读法
光靠手算容易自我怀疑,最稳的办法是写几行代码让机器告诉你答案。下面这段程序把同一串位模式按两种类型打印出来,并演示扩展和截断:
#include <stdio.h> #include <stdint.h> int main(void) { // 同一串 8 位模式,两种读法 uint8_t u = 0xFF; // 无符号读法 int8_t s = (int8_t)0xFF; // 补码读法 printf("u=%u s=%d\n", u, s); // 期望 u=255 s=-1 // 符号扩展 vs 零扩展 int8_t neg = -1; int sign_ext = neg; // 符号扩展,仍是 -1 unsigned int zero_ext = (uint8_t)neg; // 先截成 8 位再零扩展 printf("sign_ext=%d zero_ext=%u\n", sign_ext, zero_ext); // 截断:宽变窄,砍高位 int big = 0x12345678; uint8_t cut = (uint8_t)big; // 只留最低 8 位 printf("cut=0x%X\n", cut); // 期望 0x78 return 0; }逻辑说明:第一组用uint8_t和int8_t对同一个0xFF做不同解释,直接印证“位模式相同、读法不同”。第二组里sign_ext走的是补码符号扩展,zero_ext先把值截成 8 位无符号再提升,得到 255,两者对比能让你一眼看出扩展规则的分水岭。第三组演示截断只保留低位。
参数说明:uint8_t、int8_t来自<stdint.h>,宽度明确,避免依赖平台默认宽度;打印无符号用%u、十六进制用%X,格式符和类型必须匹配,否则输出本身就是错的——这本身就是第三章要你建立的类型意识。
编译运行:
gcc -Wall -Wextra -o ch3_verify ch3_verify.c && ./ch3_verify-Wall -Wextra打开全部常见警告,第三章相关的类型转换问题很多会在这里被编译器提前点出来,别忽略它们。
3.2 浮点位模式的拆解脚本
浮点题手算步骤多,写个小脚本把“位模式 → 十进制”和“十进制 → 位模式”两个方向都跑通,做完题对一遍,效率高很多:
import struct def bits_to_float(bits): # bits 是 32 位整数的位模式 packed = struct.pack('>I', bits) # 按大端打包成 4 字节 return struct.unpack('>f', packed)[0] # 按单精度解回浮点 def float_to_bits(value): packed = struct.pack('>f', value) return struct.unpack('>I', packed)[0] # 验证:0x3F800000 应该是 1.0 print(hex(float_to_bits(1.0))) # 期望 0x3f800000 print(bits_to_float(0x3F800000)) # 期望 1.0 # 验证一个负数:-2.5 print(hex(float_to_bits(-2.5))) print(bits_to_float(float_to_bits(-2.5)))逻辑说明:struct.pack('>I', bits)把整数按大端序转成 4 字节,再unpack('>f', ...)按单精度浮点解释,等价于“把位模式读成 float”。反向操作就是“把 float 写成位模式”。>表示大端,和 IEEE 754 的字段排列一致,别用<,否则字节序反了结果全错。
参数说明:'I'是无符号 32 位整数,'f'是单精度浮点,两者都是 4 字节,正好对应。手算时你可以先按 3.3 的表格拆出 s、e、f,再用这个脚本核对,两边对不上就说明手算某一步错了。
3.3 手算浮点的固定四步
拿到“给定位模式求值”的题,按这四步走,基本不会乱:
- 拆位:从 32 位里切出符号位、8 位阶码、23 位尾数;
- 判特殊:阶码全 0 或全 1 先单独处理;
- 算指数:实际指数 = 阶码 - 127;
- 拼尾数:写成 1.f 的形式,乘上 2 的指数次方,再带符号。
反过来“给定十进制求位模式”,就是这四步的逆过程:先把十进制规格化成 1.xxx × 2^n,n 加 127 得到阶码,xxx 补足 23 位得到尾数,最后拼上符号位。这套流程配合 3.2 的脚本,做完立刻能验证。
4. 高频题型逐个拆:补码运算、类型转换、浮点编码
4.1 补码加减与溢出判断
第三章里补码运算的题,考的不是“会不会加”,而是“会不会判溢出”。补码加法的溢出判断有两条常用规则:
- 同号相加,结果变号,则溢出;
- 最高位的进位和次高位的进位不同,则溢出。
第二条是硬件层面的判据,也是理解“加法器不区分有无符号”的关键。举个例子,8 位补码里 127 + 1:
0111 1111 (127) + 0000 0001 (1) ----------- 1000 0000 (-128,溢出)两个正数相加得到负数,按第一条规则立刻能判出溢出。手算时把每一步的进位标出来,再对照规则,比死记结论可靠。
无符号加法的溢出判断更简单:结果小于任一操作数,就发生了回绕。比如 8 位无符号 255 + 1 = 0,结果 0 小于 255,回绕成立。
4.2 有符号与无符号混用的经典陷阱
C 语言里最经典的坑是:一个有符号负数和一个无符号数比较,有符号数会被隐式转换成无符号数。看这段:
#include <stdio.h> int main(void) { int a = -1; unsigned int b = 1; if (a < b) { printf("a < b\n"); } else { printf("a >= b\n"); // 实际会走这里 } return 0; }逻辑说明:a是 -1,b是 1,直觉上 -1 < 1 成立。但比较时a被转成无符号数,-1 的补码是全 1,转成无符号就是 4294967295,远大于 1,所以走else分支。这正是第三章“类型转换规则”的直接应用。
参数说明:这类问题的排查手段是打开编译器的转换警告。gcc -Wsign-compare会专门提示有符号无符号比较,-Wconversion会提示隐式窄化转换。做题时如果拿不准某步转换,就写个最小例子编译一遍,看警告和输出。
4.3 浮点精度丢失与舍入
浮点题里另一类高频考点是“为什么 0.1 + 0.2 不等于 0.3”。根因是 0.1 和 0.2 在二进制下都是无限循环小数,存进 23 位尾数时必须舍入,舍入误差累加后结果就不是精确的 0.3。验证:
print(0.1 + 0.2) # 0.30000000000000004 print(0.1 + 0.2 == 0.3) # False逻辑说明:这不是语言 bug,是 IEEE 754 表示的固有特性。第三章让你手算浮点编码,目的之一就是让你亲眼看到“哪些十进制小数能被精确表示、哪些不能”。能被精确表示的分母必须是 2 的幂,0.5、0.25、0.125 可以,0.1、0.2、0.3 不行。
参数说明:比较浮点数不要用==,用误差容限,比如abs(a - b) < 1e-9。容限取多大取决于你的精度需求,单精度大约 1e-7,双精度大约 1e-15,别一刀切。
4.4 位运算题的通用拆法
第三章还会出现掩码、移位、位段相关的题。通用拆法是:先把操作数写成二进制,按位对齐,逐位算,最后再解释结果。比如“取出一个 32 位数的第 8 到 15 位”,做法是先右移 8 位,再和0xFF做与:
uint32_t extract(uint32_t x) { return (x >> 8) & 0xFF; // 先移位对齐,再用掩码截取 }逻辑说明:右移把目标位段移到最低位,掩码0xFF保留低 8 位、清掉其余位。顺序不能反:先与后移会把高位也带进来。参数说明:掩码的位数要和目标位段宽度一致,取 8 位就用0xFF,取 4 位就用0xF,写错掩码是这类题最常见的失分点。
5. 避坑与排查:第三章做题最容易翻车的五个地方
5.1 把符号扩展和零扩展记反
现象:把 16 位负数扩展成 32 位后,值变成了一个很大的正数。原因:错误地用了零扩展,高位补 0 而不是补符号位。解决:记住“补码扩展补符号位,无符号扩展补 0”,拿不准就写个最小 C 程序打印结果,别靠记忆。
5.2 浮点阶码偏移量用错
现象:手算浮点值时结果总是差一个 2 的幂次。原因:单精度偏移量是 127,双精度是 1023,两者混用或者记成 128 都会错。解决:把偏移量写在做题纸最上方,每道题都对照一遍;用 3.2 的脚本验证,对不上就回头查偏移量。
5.3 忽略阶码全 0 和全 1 的特殊情形
现象:遇到阶码全 0 的位模式,仍按1.f的规格化公式算,结果错误。原因:阶码全 0 表示非规格化数,尾数没有隐含的前导 1,实际指数固定为 -126;阶码全 1 表示无穷大或 NaN。解决:拆完位先看阶码是不是全 0 或全 1,是就先走特殊分支,别直接套公式。
5.4 有符号无符号比较不报警告就以为没事
现象:代码逻辑看起来对,运行结果却和预期相反。原因:隐式类型转换把负数变成了大正数,而编译器默认不报这个警告。解决:编译时加-Wsign-compare -Wconversion,把警告当错误看;涉及混合类型比较时,显式写强制转换,让意图清晰。
5.5 截断后没检查高位丢失
现象:把一个宽类型赋给窄类型,值莫名其妙变了。原因:截断直接砍高位,不报错。解决:赋值前先确认目标类型能容纳原值范围;如果是有意截断,加注释说明;用-Wconversion让编译器提示所有隐式窄化。
6. 把这一章变成长期能力:三个可以立刻用上的习惯
做完第三章的题,真正有价值的不是那几道答案,而是把“位模式思维”变成习惯。我自己的做法是三个:第一,任何涉及类型转换的代码,先问一句“这里会不会发生隐式转换”,拿不准就写最小例子编译一遍,看警告和输出,而不是靠脑补。第二,浮点数一律不用==比较,需要比较就写误差容限,容限大小按精度需求定,单精度 1e-7、双精度 1e-15 是常用起点。第三,手算结果一定用脚本或程序验证,第三章的题手算步骤多,错一步后面全错,用 3.2 那种几行的 Python 脚本对一遍,比反复检查快得多。
再补一个进阶技巧:如果你想把这一章吃得更透,可以试着用位运算手写一个“浮点数拆解器”,不调用struct,纯靠移位和掩码把 s、e、f 三段取出来,再拼回十进制。写的过程会逼你把每一个字段的位置和宽度都记准,写完这一章基本就刻进脑子里了。我当年就是靠这个笨办法,把补码和浮点的规则从“背下来的”变成了“推得出来的”,后来遇到任何类型相关的 bug,第一反应都是去看位模式,而不是猜。希望帮到你。
本文还有配套的精品资源,点击获取