简介:这是一份编译原理课程期末考试题及答案 PDF,面向计算机专业本科生、考研复习者及期末冲刺的考生。资源包含 1 个 PDF 文件,大小约 386KB,页面精炼但覆盖核心考点。试卷由填空题、选择题和解答题构成,内容涵盖静态与动态存储分配、规范规约、词法分析、LL(1)文法改造与预测分析表构造、FIRST 与 FOLLOW 集合求解、算符优先分析及优先函数构造、移入-规约语法制导翻译、布尔表达式与循环语句的四元式生成等经典知识点。解答题部分配有较完整的推导步骤,包括文法等价改造、句柄识别、语法树绘制、FIRSTVT 与 LASTVT 集合计算、算符优先关系表构造、循环与条件语句的四元式翻译过程等,便于读者逐步对照、验证理解。目前已有 455 人学习下载,适合考前集中刷题、系统复习和定位薄弱环节。
1. 期末考前拿到这份编译原理题库:先摸清它值多少分
期末复习最怕的不是题难,是手里没有一套带评分标准的完整答案。这份《编译原理期末考试题及答案》干的正是这件事:它没有项目代码,也没有 PPT 讲义,而是把词法分析、语法分析、语义分析与中间代码生成、代码优化、目标代码生成一整条链路浓缩成填空、选择、解答三类题,每题都挂着参考答案和评分细则。适合期末突击、考研专业课复习,也适合重修考前自查。花一个晚上用草稿纸把解答题推一遍,比刷三套没答案的模拟题有用得多,因为你能看清自己的推导到底在哪一步被扣分。
2. 题型分值拆解:填空、选择、解答题各该投入多少时间
拿到这份资料,先别急着从头做。它的结构是“多份试卷合订”,不同卷子的题型名称还会重复,比如“二、解答题”和“五、解答题”各自独立成卷。我建议先把资源过一遍,按题型把题目重新归类,然后分配复习时间——这是老手拿到题库后的第一步,能省下一大半无用功。
2.1 填空题:20 分的基础分拼的是概念成对记
填空题 20 分,每空 2 分,分布在存储分配、编译阶段划分、后缀式、属性分类、数组地址计算、局部优化范围这些点上。这类题考的不是推导,是“知道不知道”。比如第 1 题问动态存储分配方案分两种,答案是栈式动态存储分配、堆式动态存储分配;第 4 题让把x+y*z/(a+b)转成后缀式,答案是xyz*ab+/+。
我的习惯是把每道填空当成名词解释来背,但不是孤立地背一个词,而是成对记。静态存储分配配动态存储分配,栈式配堆式,综合属性配继承属性,规范推导配规范归约。这样考试时无论题目从哪个方向问,都能快速定位。另一个容易被忽略的点是第 10 题“局部优化是局限于一个什么范围内”,答案“基本块”,这个空在解答题里也会反复出现,属于承上启下的概念。填空部分不需要大量练习,考前默写两遍关键词表基本就能拿满,但要注意:错一个字就是 2 分,存储和储存这种细节别写错。
2.2 选择题:20 分的覆盖面考察概念边界
选择题一共 8 道,前 6 道单选、后 2 道多选,每问 2 分。单选部分考的是概念边界:上下文无关文法的四个组成、基本块定义、递归下降分析属于哪类方法、算符优先分析法适用于什么场景、目标程序是什么形式、文法与语言唯一性的关系。这些题看起来简单,实际上是整张卷子里最容易翻车的地方。比如“一个文法所描述的语言是唯一的,但描述一个语言的文法不唯一”,这种结论型判断如果平时没留意,考场上很容易凭感觉选反。
多选部分要特别注意评分规则。资料答案里明确写了“选对一个得 1 分且不超过满分,选错一个扣一分,扣完为止”。拿第 8 题举例,问哪些语法制导翻译采用了拉链—回填技术,答案是赋值语句、条件语句、循环语句,单选布尔表达式计算。这部分不能按“少选不如多选”的逻辑做,错选要倒扣,答题策略应该是“拿不准的选项宁可不动”。复习选择题时,我一般会把每道题涉及的术语整理成一组“易混清单”:递归下降法↔自顶向下、算符优先↔适合表达式、LR 分析法↔自底向上,把相近概念排在一起做比对记忆。
2.3 解答题:60 分的大题,命题规律落在六个题型
解答题占 60 分,是这份资源的绝对核心。我剔除了重复卷子之后,把题目归成六类:LL(1) 文法改造与 FIRST/FOLLOW 求值、算符优先关系表与优先函数、移进-规约分析与语法制导翻译输出、加注释的分析树与四元式序列、基本块划分与流图/DAG、运行期存储与参数传递。六类题几乎是标准教材的目录顺序,没有超纲内容,难度梯度也一致:先做文法变换,再做集合求值,然后才是中间代码翻译。
这六类题在卷子里反复出现,比如E→ETE|(E)|i改 LL(1) 的题至少出现两次,T→T,S|S的算符优先题也出现两次。这不是资料注水,而是给你一个信号:命题人认为这些考点是必须要掌握的。我建议按题量加权分配时间,LL(1) 和四元式各花 40% 的复习时间,算符优先 20%,其余题型作为查漏补缺。做解答题时最好用草稿纸完整写步骤,不要直接在答案上画线,因为评分规则里大量存在“少写一个扣 0.5 分”的细项,只有亲手写一遍才能暴露漏步。
| 题型 | 分值占比 | 高频考点 | 常见失分点 |
|---|---|---|---|
| LL(1) 文法改造 | 约 20% | 消除左递归、提取左因子、预测分析表 | 漏考虑#,公共左因子未合并 |
| FIRST/FOLLOW 求值 | 约 15% | 集合迭代计算 | FOLLOW 少算终结符,推导符号写错 |
| 算符优先分析 | 约 15% | FIRSTVT、LASTVT、优先表、优先函数 | 未判断“任一对终结符关系不冲突” |
| 移进-规约与翻译输出 | 约 15% | 栈模拟、语义动作执行时机 | 动作执行顺序与规约不同步 |
| 四元式与回填 | 约 25% | 分析树、三地址码、四元式序列 | 跳转编号对不上,回填位置错 |
| 基本块与存储 | 约 10% | 基本块划分、DAG、传地址/传值 | 入口出口判断错误 |
3. 三类语法分析的真题套路:LL(1)、算符优先和自底向上的读题顺序
解答题里最劝退的是语法分析部分,因为它同时考察文法的形式变换和对分析过程的理解。我把卷子里涉及的方法拆成三条线:LL(1) 系列、算符优先系列、移进-规约系列。三条线之间的逻辑其实是递进的——先有文法,再判断能不能用某种方法分析,最后落实到分析过程。
3.1 LL(1) 改造:消除左递归只是第一步
卷子里的经典题是E→ETE|(E)|i,其中T→*|+。第一眼看到这个文法会觉得别扭:E既能推出(E),又能推出ETE,而且E在右侧开头出现,这是典型的直接左递归。改 LL(1) 的第一步就是消除左递归,把E→ETE|(E)|i改写成:
E → (E) E' | i E' E' → T E E' | ε T → * | +改写原理是:把左递归的产生式A→Aα|β换成A→βA',A'→αA'|ε。这里的E'相当于是“后缀承接”,把原来E右侧所有可能继续出现的内容交给E'循环处理,而T则承接了左右操作数之间的连接符。改写完之后还要检查一件事:每个非终结符的候选式不能有公共前缀。这里E'的候选式是TEE'和ε,没有公共前缀,所以算通过。
接下来判断是否为 LL(1) 文法,本质上要看预测分析表里每个表项是否只有一个产生式。判断标准是:对同一非终结符的任意两个候选式α和β,要么FIRST(α)与FIRST(β)不相交;要么某个候选式能推出ε,此时该候选式的FIRST集合与另一候选式的FOLLOW集合不相交。大部分同学只记得消除左递归,却忘了验证这后半句,结果构造分析表时才发现表项冲突。我给一个简单的自检流程:
- 检查每个非终结符是否有直接或间接左递归,有则消除。
- 提取公共左因子,比如
A→aB|aC要改成A→aA'、A'→B|C。 - 求所有非终结符的 FIRST 和 FOLLOW。
- 按“候选式首符号”填分析表,遇到
ε用 FOLLOW 集合补项。 - 检查每个表项是否唯一,唯一则满足 LL(1),否则回头看改造。
3.2 FIRST 与 FOLLOW:机械迭代但容易漏#
FIRST 和 FOLLOW 的求解不需要技巧,需要的是“不漏”。我在做这类题时习惯用迭代算法,而不是凭眼力直接写结果。用伪代码描述这个过程:
first = {A: set() for A in nonterminals} follow = {A: set() for A in nonterminals} changed = True while changed: changed = False for A, rhs_list in productions.items(): for rhs in rhs_list: # FIRST 集合:逐个扫描 rhs 的符号 for sym in rhs: before = len(first[A]) if sym in terminals: first[A].add(sym) break else: first[A] |= first[sym] if 'ε' not in first[sym]: break if len(first[A]) != before: changed = True # 上面只是 FIRST 的示意,还需要再处理 FOLLOW,逻辑类似参数说明:nonterminals是所有非终结符集合,productions是产生式字典,terminals是终结符集合。算法的核心是“集合发生变化就继续扫描,直到一轮循环没有任何变化为止”。实际考试里不要求写出代码,但这个迭代视角能帮你理解为什么 FIRST 集合必须从直接推导的候选式开始逐步扩散,而不是拍脑袋写。
卷子里的标准答案是:FIRST(E)={(,i},FIRST(E')={*,+,ε},FIRST(T)={*,+};FOLLOW(E)={),*,+,#},FOLLOW(E')={),*,+,#},FOLLOW(T)={(,i}。注意这里每个 FOLLOW 集合都补了#,评分规则里专门写了“没考虑 # 扣 0.5 分,其它错或少写一个扣 0.5 分”。这个细节就是我反复强调评分标准的原因——很多同学集合算对了,就因为开头符号没写全被扣分。
FOLLOW 的计算有个常见的坑:E'出现在TEE'的末尾,同时E的候选式又包含E',这种情况下FOLLOW(E')会继承FOLLOW(E)的一部分,同时又受TEE'中E后面的影响。建议做题时先标注每个非终结符出现的位置,再逐条套用 FOLLOW 三条规则,别跳步。
3.3 算符优先:优先表和优先函数
算符优先的典型题是S→a|∧|(T)、T→T,S|S。第一步求 FIRSTVT 和 LASTVT,卷子里的标准答案是:
FIRSTVT(S) = { a, ∧, ( } FIRSTVT(T) = { ,, a, ∧, ( } LASTVT(S) = { a, ∧, ) } LASTVT(T) = { a, ∧, ), , }求 FIRSTVT 的规则是:对S→a这类产生式,终结符直接进集;对T→T,S这类,右边第一个符号如果是非终结符,则该非终结符的 FIRSTVT 也要并进来。LASTVT 对称。第二步构造优先关系表,把a、∧、(、)、,、#排成矩阵,判断条件是:文法中任一对终结符之间最多只能满足<、>、=三种关系中的一种,且有相邻非终结符的产生式不能出现。这道题里没有相邻非终结符,优先表也没冲突,所以是算符优先文法。
第三步求优先函数,卷子给的是两种可接受的参考值,比如f和g分别是662662和777252。优先函数构造方法并不唯一,只要对每一对满足a > b的关系有f(a) > g(b),满足a < b有f(a) < g(b),满足a = b有f(a) = g(b)即可。我一般用“有向图编号法”:把每个终结符画成两个结点,根据优先关系连边,最终编号就是优先函数值。如果发现无法赋值,说明优先表本身就存在问题,要回头检查文法。
3.4 自底向上与移进-规约:输出串怎么算
移进-规约题的考察方式有两种,一种是纯栈模拟,另一种是配合语法制导翻译输出字符串。卷子里有这道题:
A → aB { print "0" } A → c { print "1" } B → Ab { print "2" }输入是aacbb,问打印的字符串是什么。这道题的核心是“每次按产生式规约时,立即执行对应动作”。用栈模拟的简化过程是:先移进a,看到aB可以规约但在当前输入下不能确定,于是继续移进a,再移进c,此时用A→c规约打印1,随后移进b,用B→Ab规约打印2,再对前面的aB规约打印0,如此直到栈底只剩A。最终输出是12020。
这类题最容易错的是动作执行顺序:规约是从栈顶往下找句柄的,打印动作发生在“用哪条产生式规约”的那一瞬间,不是在分析完成后统一输出。另一个易错点是“过早判断句柄”,比如看到栈里有aB就急着规约,但右侧输入还没看完,可能更长的aBc才是正确的可规约串。做题时先画栈,再标输入指针位置,每一步只允许移进或规约一个动作,动作之间不能跳。
4. 四元式与回填技术:把翻译题从背结论变成走流水线
四元式翻译是卷子里公认最难啃的部分,但它其实是一个可以流程化的操作。我从这份资源的答案里提炼出的套路是:先画加注释的分析树,再定跳转标号,最后写四元式序列。只要按这个顺序走,变体题也能做。
4.1 先画带注释的分析树:定位 M.quad 和 E.truelist
以while (a<b) do if (c>d) then x:=y+z为例。标准做法是先在语法树上标出M1、E1、M2、S1这些结点。M→ε这个产生式看起来很怪,它的作用是在翻译时记录“当前四元式编号”。当扫描到while后面的条件时,M1记录的是条件判断开始前的编号;当扫描到do后面的语句时,M2记录的是循环体开始前的编号。E.truelist和E.falselist则分别表示条件为真和条件为假时,需要回填的跳转指令位置。
我见过的考生普遍卡在“M 到底代表什么”上。它不代表任何运行时代码,只代表一个“锚点”,相当于给四元式序列插一个书签。画分析树时,把每个M结点写在它实际插入的语法位置,然后在旁边标注它的编号值。这道题里M1.quad=100,M2.quad=102,后续写四元式时所有跳转目标都以这些锚点为准。
4.2 四元式序列:把连续行当成逻辑地址
拿到锚点之后,四元式序列就不难写了。以同类题while (x>y) do if (a=b) then x:=2*y+a为参考,标准四元式如下:
100 (j>, x, y, 102) 101 (j, -, -, 108) 102 (j=, a, b, 104) 103 (j, -, -, 100) 104 (*, 2, y, t1) 105 (+, t1, a, t2) 106 (=, t2, -, x) 107 (j, -, -, 100) 108 (-, -, -, -)逐行解释:100 是if (x>y)的真跳转,条件成立跳到 102 继续处理内层 if;101 是条件为假的兜底跳转,直接跳到 108 的出口;102 是内层if (a=b)的真跳转,跳到 104 执行赋值;103 是内层假跳转,跳回 100 的循环条件重新判断;104 计算2*y存入临时变量t1;105 计算t1+a存入t2;106 把t2赋给x;107 是循环体执行完后的回跳,重新检查外层条件;108 是占位四元式,表示整个 while 语句的出口。
参数说明:每个四元式由四部分组成(运算符, 操作数1, 操作数2, 结果),跳转指令里用-表示空操作数。这里编号从 100 开始是人为设置的基准,实际从几开始都行,关键是跳转目标必须与编号严格一致。我看过很多同学的答案,四元式本身写对了,但 103 行写成goto 101,整条逻辑链就断了,扣分往往就扣在这种地方。
4.3 回填:什么时候写死编号,什么时候先留空
回填技术是这套翻译方案的核心。原始答案解析里反复出现backpatch(E.truelist,M.quad)之类的语义动作,它说的是:当E的真链和假链在翻译初期还没有确定目标时,先用链表记下所有等待回填的四元式下标,等后面的M或S.nextlist确定后,再一次性把跳转目标写进去。
常见做法是:做题时先在跳转目标处写0占位,全部四元式写完后,再根据M1.quad、M2.quad这些锚点回头补填。这样能避免边写边算导致的跳转目标错位。回填的对象有两个,一个是E.truelist,另一个是E.falselist,它们分别对应条件成立和条件不成立时的出口。merge函数负责把两个链表合并成一条,它做的事就是让多个跳转指令指向同一个后续目标。理解了回填,再看卷子里给的语义动作公式就不会晕,因为每个backpatch都对应一次“把占位数字换成真实编号”的操作。
5. 常见问题与排查:答案排版、评分标准和复习习惯里的五个坑
这套资料下载下来能直接用,但使用过程中有几个实际坑,我按“现象 → 原因 → 解决”的方式列出来,提前知道能少走弯路。
5.1 PDF 内公式显示乱码或字符缺失
现象:部分填空题答案里的希腊字母ε、∧显示成方框或乱码,四元式序列里数字挤在一起,个别产生式右部缺符号。原因:原始文档从 Word 或图片转 PDF 时字体映射出错,卷子合订过程中也丢过排版信息。解决:遇到乱码先按上下文占位,比如产生式E'→TEE'|ε中ε缺失,就根据“候选式可推出空”这个语义补上,再对照教材确认。四元式如果挤在一起,我会先用纸把每行四列拆开重写一遍,再判断逻辑。
5.2 多份试卷合订导致题号重复混淆
现象:资料里有多套卷子,“二、解答题”出现不止一次,做的时候容易把前一卷的答案安到后一卷的题上。原因:合集没有统一重命名,各卷保留了自己的原始题号。解决:做题前先按我第 2 章的题型归类表,给每道题贴一个唯一标识,比如“文法 G[I] 选择题”“文法 G[S] 算符优先题”。标识用题干里的第一句话,不要用题号。
5.3 多选题评分规则与常规习惯冲突
现象:按“多选少选都不得分”的常规思路做多选题,结果被“选对一个得 1 分、选错一个扣一分”打乱节奏,要么不敢选,要么乱选。原因:考前没有先读答案后面的评分说明。解决:拿到资料后第一件事是翻到末尾的参考答案,先把每题后面的评分规则读一遍,再开始做题。遇到选错倒扣的题,把确定性不高的选项全部跳过。
5.4 背熟原题之后遇到变量替换就卡壳
现象:把while (a<b) do if (c>d) then x:=y+z的四元式答案背得滚瓜烂熟,考试换成while (x>y) do if (a=b) then x:=2*y+a就写不出来。原因:记忆的是整个具体表达式的翻译结果,而不是“画树 → 标 M → 定锚点 → 写四元式 → 回填”的处理流程。解决:按第 4 章的流水线方法,自己换三组条件表达式做变体练习,比如把a<b改成x>=y,把赋值语句改成a:=b*c+d,练到不看答案能完整推出四元式为止。
5.5 填空题的对偶概念记混
现象:综合属性和继承属性分不清,静态存储分配和动态存储分配的适用语言记反。原因:复习时把概念孤立记忆,没有做配对。解决:把填空题的答案按“一组对偶概念”重新整理:静态↔动态、栈式↔堆式、综合属性↔继承属性、规范推导↔规范归约、自顶向下↔自底向上。记忆时只背一对词,考试时看到其中一个就能想起另一个。
6. 用评分标准反向自测:三轮复盘把错题变成提分点
最后分享一套我自己用这套题库做考前自测的方法。方法核心是“让评分标准当老师”,不是做完题对一遍答案就完事。
第一轮:按题型过。把解答题当正式考试做,每题都在草稿纸上写完整步骤,不跳步。做完之后只对答案的最终结果,先不看评分细节。这一轮的目标是暴露自己“哪个题型完全不会”,而不是统计分数。自检标志是:随便指一道题,你能立刻说出它属于 LL(1)、算符优先、移进-规约、四元式还是基本块题目。
第二轮:按评分标准扣分。逐字对照答案里的细则给自己打分,FIRST/FOLLOW 少算一个#扣 0.5 分,LL(1) 分析表错一格扣 0.5 分,推导过程错写符号扣 0.5 分,四元式少写一行扣 0.5 分。这轮的目的不是拿高分,而是找出“看起来很对但经不起细看”的步骤。比如我当年做算符优先题,优先关系表能填对,但在判断“是否算符优先文法”时漏写了“无相邻非终结符”这个条件,被扣了 2 分,这种细节只有按评分标准比照才能发现。
第三轮:限时整套模拟。给自己 120 分钟做完整套卷,目标客观题拿满 40 分,解答题按评分标准折算拿 48 分以上。模拟时把手机扔到另一个房间,草稿纸准备够,做完立刻按评分标准自评。自评时把扣分点记录在一张草稿纸上,不写题目本身,只写扣分原因,比如“FOLLOW 少了 #”“回填编号写错”“句柄没标下划线”。考前一天只看这张纸。
| 轮次 | 动作 | 自检标志 |
|---|---|---|
| 第一轮 | 按题型做解答题,不跳步 | 每题能说出所属知识点 |
| 第二轮 | 按评分标准逐项扣分 | 能指出自己被扣分的具体位置 |
| 第三轮 | 120 分钟整套限时模拟 | 总分折算稳定在 88 分以上 |
这套方法的额外好处是,评分标准读多了,你会慢慢形成一种“命题人视角”:看到填空能预判答案里的关键词,看到分析题能预判评分规则里那句“少写一个扣 0.5 分”落在哪里。从那以后,我每次拿到这类带答案的复习资料,第一件事都不是刷题,而是先把评分标准读三遍——第一遍扫结构,第二遍对照自己的答题习惯找隐患,第三遍把最容易扣分的点默写在草稿纸上。准备好框架之后再去推解答题,整套刷下来错的题反而比随意刷少很多。希望这套方法对你也同样管用。
本文还有配套的精品资源,点击获取