前天晚上一个学弟发消息给我,说《计算机组成原理(微课版)》第三章的课后题他刷了两遍,合上书重做还是会在符号位上翻车:原码、补码、反码、移码四个码换过来换过去容易乱,补码加法的溢出判断也是靠死记。我回了一句:你不是不会算,你是没把“运算方法”和“运算器”这两条线串起来。这章叫运算方法与运算器,表面上是四个码、四类运算,实际考的是同一件事——硬件的运算器到底怎么把二进制算出来。这篇文章我就按课后题最常出现的几大题型,把答案和推导过程一起拆开讲,适合正在刷课后题、准备期末或者准备考研基础复习的人。
## 1. 这一章的“骨架”:数据表示、四类运算、运算器结构 第三章的内容看上去很杂,但骨架就三条:数据表示、算术运算、运算器实现。多数课后题也是围绕这三条线展开的,理解了这个框架,你才知道一道题考的是“机制”还是“电路”。 ### 1.1 四个码之间的关系,不是背出来的 原码、反码、补码、移码,所有教材都会给定义,但真正算题时会发现,转换关系其实是一条链: - 正数的原码、反码、补码一样; - 负数的反码是原码数值位按位取反; - 补码是反码再加 1; - 移码是补码的符号位取反。 从硬件角度看,补码是最省事的表示方式。因为补码把减法变成加法,符号位也当成数值位参与运算,运算器里只需要一套加法器。移码则是为了让浮点数的阶码可以按无符号数比较大小,所以浮点题里经常碰到。 课后题如果让你“写出 x 的原码、反码、补码和移码”,不要一个一个硬算,而是先写原码,再推反码,再推补码,最后推移码。这样一条链走下来,出错概率小很多。 ### 1.2 加、减、乘、除分别对应什么硬件套路 加减法对应加法器和进位链;乘法对应“部分积累加 + 右移”,原码一位乘和补码一位乘(Booth 算法)都是这个思路;除法对应“移位 + 减法”的迭代过程,恢复余数法和加减交替法本质是同一套比较逻辑。运算器结构题则常考 ALU、寄存器、数据通路,以及进位方式对速度的影响。 这些内容在课后题里很少孤立出现,通常是一道大题给你一串二进制数,让你完成一种运算,再问你结果是否溢出,或者让你画出运算器的数据通路。所以复习的时候要主动把“数据表示”和“运算方法”连起来。 ### 1.3 我建议的刷题顺序 我通常会让学生按这个顺序过课后题: 1. 先练码制转换和真值范围,这是所有运算的基础; 2. 再练补码加减法和溢出判断,因为乘除法都要用补码; 3. 接着是原码一位乘、补码一位乘,重点观察部分积右移; 4. 然后是除法,恢复余数法能看懂,加减交替法能写出步骤; 5. 最后是浮点运算,把对阶、求和、规格化、舍入、判溢出串起来。 这样每走一步,都是在给下一步铺路,不会出现“乘法算到一半符号位乱套”的问题。2. 码制转换和溢出判断题:核心是“模”的概念
课后题里最基础的题型就是给你一个十进制真值,让你写出四种码,再算两个补码相加判断是否溢出。这类题不难,但特别容易在小数点、位长、符号位上丢分。
2.1 典型题:8 位字长,求 (x=-53D) 的四种码
先把 53 转成二进制:(53D = 32+16+4+1 = 00110101B)。因为是 8 位字长,最高位留给符号位,所以:
- 原码:符号位为 1,数值位不变,得到
10110101 - 反码:符号位不变,数值位按位取反,得到
11001010 - 补码:反码加 1,得到
11001011 - 移码:补码的符号位取反,得到
01001011
这里有一个很多初学者都会问的问题:为什么补码要“反码加 1”?因为补码的定义是模 (2^8) 意义下的余数表示。负数 (x) 的补码等于 (2^8 + x),而 (-53) 对应 (256-53=203),写成 8 位二进制正是11001011。“反码加 1”只是这个模运算的快速计算方式,不是人为规定的技巧。
2.2 典型题:补码加法到底溢出了吗
看一个经典题:设 8 位字长,补码表示,求 (127D + 1D),并判断是否溢出。
- (127D = 01111111)
- (1D = 00000001)
相加得到10000000,在补码里这是 (-128D)。一个正数加正数,结果变成了负数,显然不合理,所以溢出。
判断溢出不只有“正加正得负”这种直观方法,还有两个更通用的判断标准:
- 最高有效位进位与符号位进位的异或结果为 1 时,溢出。这个例子中,最高有效位上的 (1+1) 产生进位,而符号位 0+0 加上这个进位后不产生进位,两者相异,所以溢出。
- 使用双符号位(变形补码)判断。把符号位扩展成两位,运算后符号位变成
01表示正溢出,10表示负溢出。
2.3 双符号位判断溢出的原理
双符号位教材上写得比较抽象,但实际操作很简单:先给每个数添一个符号位副本,比如+127写001111111,+1写000000001,相加得到010000000。两个符号位分别是 0 和 1,也就是01,这就是正溢出。
为什么01就一定是溢出?因为正常结果用两个相同的符号位表示:00是正,11是负。一旦运算结果的符号位变成01或10,说明结果的数值部分增长到了符号位,原来的字长已经装不下了。这个方法在乘除法、浮点运算里同样适用,建议养成写双符号位做题的习惯。
## 3. 加法器与进位链:串行进位和组间串行进位的延迟估算 这一节课后题经常从“一位全加器”出发,逐步让你推出串行加法器、并行进位加法器、组间串行进位加法器。很多人看到门电路就头疼,其实抓住一个核心就行:进位是逐位传还是提前算。 ### 3.1 一位全加器的逻辑表达式 一位全加器有三个输入:两个加数 \(A_i\)、\(B_i\) 和低位进位 \(C_i\);两个输出:本位和 \(S_i\)、向高位的进位 \(C_{i+1}\)。 - \(S_i = A_i \oplus B_i \oplus C_i\) - \(C_{i+1} = A_iB_i + (A_i \oplus B_i)C_i\) 这里 \(A_iB_i\) 被称为进位生成函数 \(G_i\),它的含义是:当两个加数都是 1 时,无论低位有没有进位,本位一定会向高位产生进位。\((A_i \oplus B_i)\) 被称为进位传递函数 \(P_i\),含义是:当两个加数中只有一个为 1 时,如果低位有进位进来,就会原样传出去。 课后题如果让你“写出全加器表达式并说明含义”,把 \(G_i\) 和 \(P_i\) 这两个名字写上,基本能拿全分。 ### 3.2 4 位超前进位加法器的进位级联 串行加法器的问题在于进位要一位一位往上抬,最坏情况是最低位进位一路传到最高位。超前进位加法器就是为了消除这种等待,用逻辑直接把 \(C_1\) 到 \(C_4\) 同时算出来。 以 4 位为例,假设 \(G_i = A_iB_i\),\(P_i = A_i \oplus B_i\),则: - \(C_1 = G_0 + P_0C_0\) - \(C_2 = G_1 + P_1G_0 + P_1P_0C_0\) - \(C_3 = G_2 + P_2G_1 + P_2P_1G_0 + P_2P_1P_0C_0\) - \(C_4 = G_3 + P_3G_2 + P_3P_2G_1 + P_3P_2P_1G_0 + P_3P_2P_1P_0C_0\) 这组公式不用死背,理解规则就好:\(C_i\) 的表达式里,要么由某个高位位置的 \(G_j\) 直接产生,要么由一串 \(P_j\) 把初始进位 \(C_0\) 传过来。写成“与或式”后,所有进位可以在同一个周期内算完。 ### 3.3 16 位 ALU 采用组间串行进位的延迟估算 考试特别爱考“16 位 ALU 分成 4 个 4 位一组,组内先行进位,组间串行进位,求最坏进位延迟”。 设每个 4 位组内部产生组进位的时间为 \(T\),组间进位传递时间也为 \(T\)。第一组需要 \(T\) 时间先算出组进位,然后这个进位要依次传给第二组、第三组、第四组,共经过后 3 个组间传递,也就是 \(3T\)。总延迟约 \(T+3T=4T\)。 如果把 16 位全部采用行波进位,每一位的进位都串联,延迟约 \(16T\),这就能看出分组并行进位的优势。答题时要说明:组内并行缩短了进位链长度,组间串行只保留少量串行时间,这是速度和复杂度之间的折中。4. 乘法器课后题:原码一位乘和 Booth 算法的手算套路
乘法是第三章课后题里的重头戏。原码一位乘相对简单,补码一位乘(Booth 算法)则容易在“加还是减”上判断错。我建议每道题都用真实二进制数完整推一遍,不要只看最终答案。
4.1 典型题:原码一位乘 (x=-0.1101),(y=+0.1011)
原码一位乘的核心思想是“符号位单独处理,数值位按绝对值相乘”。
先算符号位:乘数、被乘数异号,结果符号为负,符号位为 1。
接着算数值部分:(0.1101 \times 0.1011),手算竖式如下:
0.1101 × 0.1011 --------- 0.1101 0.1101 0.0000 0.1101 --------- 0.10001111所以 (x \times y = -0.10001111),写成原码为1.10001111。
实际硬件流程是:用乘数最低位判断是否加被乘数,每次加完后把“部分积 + 乘数”整体右移一位。因为乘数和部分积最终拼成了乘积,右移操作既是为了让部分积的一位落入乘数寄存器,也是为了把结果逐步凑出来。
4.2 Booth 算法:补码一位乘到底看哪两位
考试时 Booth 算法通常给你两个小数,要求用补码乘法求积。常用判断表是:
| 当前乘数位 (y_i) | 附加位 (y_{i+1}) | 操作 |
|---|---|---|
| 0 | 0 | 不操作 |
| 0 | 1 | 加 [x]补 |
| 1 | 0 | 减 [x]补,即加 [-x]补 |
| 1 | 1 | 不操作 |
注意,这里的“看两位”是从最低位和附加位开始,每一步根据这两位的组合决定操作,然后做一次算术右移。算术右移的意思是符号位保持不变,这样补码的符号位不会因为右移而丢失。
我建议考试时先用普通乘法验证结果。比如 (x=+0.1101),(y=-0.1011),普通竖式算出来积为 (-0.10001111),那么补码形式就是1.01110001。做完 Booth 表之后如果结果不是这个数,说明某一步的“加”或“减”判错了。
4.3 这两道乘法题最容易踩的坑
第一个坑是原码一位乘把符号位也拿去参与乘法运算。正确的做法是先做异或得到符号位,数值位绝对值相乘,最后再拼符号位。
第二个坑是 Booth 算法右移时用了逻辑右移。补码是负数时,右移必须在高位补 1,也就是算术右移。很多人在这一步把1001右移成0100,结果整个结果都不对。
第三个坑是漏掉乘数最低位右侧的附加位。Booth 算法初始化时一定要在乘数最低位后面补一个 0,每一步判断的是当前最低位和这个附加位的组合,不是一个位。没有附加位,整个判断循环就错位。
5. 除法器课后题:恢复余数法的完整推演
除法比乘法更抽象,因为它不是单纯的“加加加、移移移”,而是每一步都要判断“够不够减”。恢复余数法和加减交替法是本节两大考点。
5.1 典型题:原码恢复余数法 (x=0.0110),(y=0.1100)
设被除数 (x=0.0110),除数 (y=0.1100)。我先用竖式感觉一下结果:(0.0110 / 0.1100 = 0.1000),余数为 0。
恢复余数法流程:
- 余数寄存器 R 初始值为被除数
0.0110,商寄存器 Q 初始为0000; - 每一轮先让 R 左移一位,然后减去除数 M;
- 如果减完后大于等于 0,商上 1,余数寄存器保留减后的结果;
- 如果减完后小于 0,商上 0,同时把减掉的除数加回来,恢复到移位后的原始值,所以叫“恢复余数”。
完整过程如下:
| 步骤 | 操作 | 比较结果 | 商 |
|---|---|---|---|
| 初始 | R=0.0110, M=0.1100 | — | Q=0000 |
| 第1轮 | R 左移为0.1100,减 M 得0.0000 | ≥0,商1 | Q=0001 |
| 第2轮 | R=0.0000,左移减 M 得负 | <0,恢复,商0 | Q=0010 |
| 第3轮 | R=0.0000,左移减 M 得负 | <0,恢复,商0 | Q=0100 |
| 第4轮 | R=0.0000,左移减 M 得负 | <0,恢复,商0 | Q=1000 |
最终商为0.1000,余数为0.0000。验证一下:(0.1000 \times 0.1100 + 0 = 0.0110),正确。
这种题考的是对“够减/不够减”的循环理解,所以我建议每轮都标注“恢复前”和“恢复后”的 R 值,阅卷老师看了会觉得你很稳。
5.2 加减交替法为什么可以省掉“恢复”
加减交替法也叫不恢复余数法。它的思路是:如果某一步不够减,不要立刻把除数加回去,而是把“减多了”的这个结果保留,下一步用“左移后加除数”来修正。
规则可以这样记:上一步商 1,下一步就减除数;上一步商 0,下一步就加除数。整个过程不断根据上一步的正负决定下一步的操作,所以省去了反复恢复余数的步骤。
从硬件角度来说,恢复余数法的缺点在于“不够减”时要多做一次加法,这既浪费时间,也会让最坏执行时间不确定。加减交替法让每一步固定就是一次移位加一次加/减法,执行时间稳定,控制逻辑也简单。
5.3 做除法题时的符号位和余数修正
除法题和乘法一样,符号位单独处理。原码除法里,商符号 = 被除数符号异或除数符号;余数符号通常和被除数一致。
这里有一个容易忽略的细节:如果题目要求余数,不能只写商。余数也有符号,而且余数的位数可能需要修正。课后题里如果给了“原码一位除法”这个前提,最终结果一般写成:
- 商用原码表示;
- 余数用原码表示,符号位与被除数相同。
题目没给符号位时,可以先用正值算数值部分,最后再补符号位,这是我比较推荐的做法。
6. 浮点运算题:对阶、求和、规格化、舍入、判溢出
浮点运算把整章知识都串起来了。很多同学看到“阶码”“尾数”就慌,其实浮点运算的课后题套路比整数乘除法还固定。
6.1 典型题:浮点加减法五步走
看一道题:设浮点数格式为 3 位阶码、4 位尾数,求
[ x=2^{011} \times 0.1001, \quad y=2^{001} \times 0.1011 ]
的和。
第一步对阶。阶码差为 (011 - 001 = 2),按照“小阶向大阶看齐”的规则,把阶码较小的 (y) 的尾数右移 2 位:
[ 0.1011 \rightarrow 0.001011 ]
第二步尾数求和:
[ 0.1001 + 0.001011 = 0.101111 ]
第三步规格化。看最高有效位是不是 1,这里0.101111最高有效位已经是 1,不需要左规格化。
第四步舍入。如果尾数只保留 4 位,0.101111需要处理多出来的低位。按就近舍入可以约成0.1100,按截断则保留0.1011。不同教材默认舍入方式不同,做题时要看清题干的约定。
第五步判溢出。阶码没有超过 3 位能表示的范围,结果可表示。最终结果约为:
[ 2^{011} \times 0.101111 ]
我实际验算过:(x) 是 (8 \times 0.5625 = 4.5),(y) 是 (2 \times 0.6875 = 1.375),两者相加为 5.875,也就是 (8 \times 0.734375),和上面算出来的尾数一致。
6.2 浮点乘除法只看阶码和尾数
浮点乘除法运算相对简单:阶码相加(或相减),尾数相乘(或相除),最后规格化和舍入。
乘法里要注意,阶码如果用的是移码,相加之后要减掉一个偏置常数。很多教材用的偏置是 (2^{n-1}),题目如果没明确说,按默认偏置算就行。尾数相乘的符号位处理规则和整数乘法一致,同号得正、异号得负。
6.3 最容易丢分的阶码溢出问题
浮点数的溢出不是看尾数,而是看阶码。尾数最高位进位并不代表真溢出,因为可以通过右规格化把尾数放回去,同时让阶码加 1。真正危险的是阶码自己超过上限。
课后题里常出现这种情况:尾数相加后是1.1011,你以为是溢出,其实只要把结果右移一位变成0.11011,阶码加 1,就重新变成规格化数。阶码如果已经到最大值,再加 1 就是上溢,这时候计算机才会报“溢出”。
所以做题写“溢出”两个字前,先问自己:是尾数溢出可以修,还是阶码溢出没法修?前者是正常现象,后者才是真错误。
7. 这份解析之外的几条实战建议
题目做对了,不代表这章就学会了。我见过很多学生课后题答案背得很熟,但换一组数据就懵,问题就出在只记住了“这一题的步骤”,没有记住“这一类题的结构”。
7.1 把答案升级成“推导过程”
做错题之后,不要只在旁边改个数字,哪怕只是标注一句“因为符号位进位和数值最高位进位不同,所以溢出”,都会让下次刷题顺畅很多。真正有价值的笔记,不是把正确答案抄一遍,而是把踩坑点写清楚。
7.2 用一张数据通路图串起整章
学完这章后,可以自己画一遍 16 位 ALU 的数据通路:寄存器 A、寄存器 B、ALU、移位器、乘商寄存器、结果总线。每画一次,加减乘除的硬件流程会清晰很多。
7.3 和笔记、期末卷配合使用的姿势
很多人会找王道计算机组成原理笔记或者期末试卷来刷。我的建议是:课后题用来打基础,笔记用来查漏补缺,期末卷用来卡时间训练。不要一上来就做名校期末卷,第三章的概念还没理顺时,做综合卷很容易被浮点题劝退。
最后提一个我试验过很多次的小技巧:每次刷完第三章,把每类题里的数字换掉,重新做一遍。上午做加法、下午做乘法,隔一天再全做一次。运算方法这章特别适合“间隔重复”,因为它的计算步骤多,但题型极其固定,只要形成肌肉记忆,期末基本不会失分。