编译原理期末试题含答案:词法分析、语法分析、中间代码考点精讲

发布时间:2026/10/3 1:17:20
编译原理期末试题含答案:词法分析、语法分析、中间代码考点精讲 简介这份《编译原理》期末试题含答案(八)是为高校计算机专业学生准备的备考资料重点覆盖词法分析、语法分析、语义分析与中间代码生成等核心考点。资源内为1份docx文档压缩包仅19KB体量精简可直接打印或离线阅读。目前已吸引85人学习下载。试题包含正则表达式DFA状态转换图、LR(1)文法构造、LL(1)分析表填写、语法制导定义与翻译方案设计以及for语句中间代码结构、栈内存分配、作用域与生存期辨析、C语言类型系统缺陷和编译器移植路线等典型题目每道题均附有参考答案与步骤说明。尤其适合在期末冲刺阶段用来快速检验自己对编译原理各模块的理解也可作为教师出题或备课的参考资料。1. 期末复习最该先拿到的不是教材是带着答案的真题《编译原理》期末试题含答案(八)是一份典型的期末复习材料核心价值在于它把词法分析、语法分析、中间代码生成、代码优化这些硬核考点浓缩成了一份可自测的考卷。对正在备战期末的本科生来说这份材料解决的是“不知道考什么、不知道答到什么程度算对”的问题对考研或补基础的人来说它则是一张现成的知识点地图能快速定位自己哪块还没吃透。我见过太多人抱着教材从第一章啃到第八章最后连LR分析表怎么填都没搞明白而直接拿一套带答案的真题卷从大题反推考点效率要高得多。这份材料适合三类人考前想快速过一遍考点的人、做题后需要标准答案对照的人、想通过题型反推重点章节的人。它的用法不是看一遍题目再瞄一眼答案而是严格按考试时间做一遍再逐题对答案、追错因。下一章先说清楚这张卷子的考点布局再讲怎么动手把答案变成自己的本事。2. 从一道填空题看整张卷子的考点布局词法、语法、中间代码三分天下一份典型的编译原理期末试卷分值分布通常不是均匀的。词法分析、语法分析这两章能占到50%到60%的分中间代码生成和代码优化再占25%到35%剩下的是编译概述、符号表、运行时存储组织这类“小分点”。这套(八)的题型如果按常规期末卷推断应当包括填空、选择、简答、大题四种其中大题的分布几乎可以锁定在以下几个板块。2.1 词法分析必考的两个出口正则式转NFA、NFA确定化与最小化词法分析这一章期末卷里最常见的两种考法是给出一个正则式要求画出NFA或者给出一个NFA要求确定化并最小化。前者考的是Thompson构造法的熟练度后者考的是子集构造法和Hopcroft最小化算法。先看正则式转NFA。Thompson构造法有固定的套路每个字符构造一个两状态NFA片段然后用epsilon转移把片段拼接起来。关键规则是连接运算符“先串后并”左片段接收态变右片段起始态并运算符“新增两个状态左右各射一条epsilon边”闭包运算“新增两个状态四条epsilon边”。这部分建议对照答案时重点检查epsilon边的方向是否画反。很多同学在闭包运算时习惯把回边画成从末尾指回开头但Thompson构造法里闭包的回边是“新增起始态指向原起始态、原接收态指向新增接收态”不是直接把原接收态连回原起始态。方向一错后面确定化时状态集合就全不对了。再看NFA确定化。确定化用的是子集构造法核心是closure闭包和move移动两个运算。步骤是从起始态出发求其epsilon闭包作为第一个状态集对每个符号求move后的epsilon闭包重复直到没有新状态集出现。这里有个常见的失分点求epsilon闭包时不考虑从某状态通过epsilon能到达的所有状态只算了直接可达的。比如状态1能通过epsilon到状态2状态2又能通过epsilon到状态3那么状态1的闭包是{1,2,3}不是{1,2}。少算一层后面整个状态转移表就全错。考试时的建议是每个闭包写完整不要跳步确定化后的状态用大写字母或数字编号新状态集出现一次就编一个号最后检查是否有未处理的符号列。NFA最小化相对独立。最小化算法是划分子集先按终态和非终态分成两组再检查每个状态在某个符号下是否跳转到同一组。如果同一个符号下跳向了不同组这个状态所在的分组还要继续切分。注意最小化是在DFA上做的不是直接在NFA上做卷子上如果要求最小化得先确定化再最小化缺一步不给分。2.2 语法分析两道大题LL(1)和LR(0)你至少得会一个语法分析这一章是整张卷子的重头戏通常出两道大题。一道是LL(1)分析——给文法要求消除左递归、提取左公因子、求FIRST集和FOLLOW集、构造预测分析表另一道是LR分析——给文法要求构造LR(0)或SLR(1)分析表。这两道题的分值加起来通常超过20分是决定及格还是优秀的胜负手。LL(1)的第一步是改写文法。左递归是LL(1)分析的死敌比如文法 E → E T | T 就直接违反LL(1)要求得改写为E → T E E → T E | ε这个改写有个容易出错的地方改写后别忘记重新求FIRST集和FOLLOW集因为新增的E会产生新的ε产生式会影响FOLLOW集的传播。FOLLOW集的求法里最隐蔽的坑是“A → αBβ且β能推导出ε时FOLLOW(B)还要加上FOLLOW(A)”。这个规则在考试里几乎是必考的不注意到就会少算一个终结符。LR分析的大题重点是活前缀和项集规范族。构造LR(0)项目集时要执行两步先将拓广文法的开始符号的闭包加入再对每个符号做GO函数转移。如果某个项目集里同时出现了移进项目和归约项目这就是冲突可能是移进-归约冲突或归约-归约冲突。试卷答案里如果给出了SLR(1)分析表说明用了FOLLOW集来解决部分冲突但注意不是所有冲突都能用FOLLOW集消除。这里分享一个做题技巧拿到LR题目先判断文法是不是二义的如果文法有二义性LR分析表一定存在冲突不必花时间消解直接说明“该文法不是LR文法”就行。部分同学在这道题上死磕半小时其实是题目本身就是用来考“你是否能识别非LR文法”的。2.3 中间代码生成逆波兰式和三地址码二选一或都要中间代码这一章常考的题型是给出一个表达式或语句要求写出其逆波兰式后缀式、三地址码或者画出语法树。逆波兰式是栈的经典应用遇到操作数输出遇到运算符比较优先级括号单独处理。三地址码则要求引入临时变量每步运算只能包含一个运算符。表达式a : b c * (d - e) / f的三地址码写法t1 : d - e t2 : c * t1 t3 : t2 / f t4 : b t3 a : t4对照答案时注意两点。第一除法除法运算符的结合性与优先级是否体现乘除比加减优先所以先算d减e再乘c后除f最后加b第二临时变量编号是否连续如果中间跳了序号说明写的时候丢了一步运算。还有一种情况是有些教材要求把结果直接赋给a不生成t4这个按教材约定来答案一般会遵循某一本指定教材的规则。四元式也是常考形态本质是三地址码的变体只是把每个运算写成(op, arg1, arg2, result)四元组。有些学校喜欢在这一题后面跟一个小问画出该表达式的DAG图这其实是在为后面的代码优化做铺垫。DAG图的构造要点是合并公共子表达式答案里如果出现一个节点被两个父节点引用那就是考点。3. 把答案变回过程用脚本复现三大必考大题看答案只能确认“我哪里错了”但真想弄懂“为什么这么算”得动手把过程走一遍。这里分享三个我用过的复现方法都是靠写脚本把考试大题从手算变成自动验证跑熟了之后回头再看手算题思路会清晰得多。3.1 用Python实现一个迷你词法分析器验证正则题答案正则式转NFA、NFA确定化这两道题可以用一个不到100行的Python脚本来验证答案对不对。这里给一个最简实现先用字典描述NFA再写closure和move两个函数最后做子集构造。# 用字典表示NFA: 键为(状态, 符号)值为目标状态列表 # 用e表示epsilon nfa { (0, e): [1, 2], (1, a): [3], (2, b): [3], } def epsilon_closure(states, nfa): 求状态集的epsilon闭包反复找e转移直到不再新增 stack list(states) closure set(states) while stack: s stack.pop() # 只找从s经过epsilon能到的状态 for d in nfa.get((s, e), []): if d not in closure: closure.add(d) stack.append(d) return closure def move(states, symbol, nfa): 求状态集在某个符号下的转移目标集合 result set() for s in states: for d in nfa.get((s, symbol), []): result.add(d) return result def determinize(nfa, start, alphabet): 子集构造法把NFA转成DFA状态表 start_closure frozenset(epsilon_closure({start}, nfa)) dfa_states [start_closure] # DFA状态列表 dfa_trans {} # DFA转移表 index {start_closure: 0} queue [start_closure] while queue: current queue.pop(0) for sym in alphabet: # 对每个符号求move再求闭包 next_states frozenset(epsilon_closure(move(current, sym, nfa), nfa)) if not next_states: continue if next_states not in index: index[next_states] len(dfa_states) dfa_states.append(next_states) queue.append(next_states) dfa_trans[(index[current], sym)] index[next_states] return dfa_states, dfa_trans # 执行从0号状态开始字母表为a,b states, trans determinize(nfa, 0, [a, b]) print(DFA状态集合:, [sorted(s) for s in states]) print(DFA转移表:, trans)闭包函数是这段脚本的核心。它用栈反复迭代直到闭包不再增长这正是epsilon闭包的准确定义。执行后输出DFA状态和转移表拿来和你手算的卷面答案逐项对比有出入的地方就是在闭包计算中遗漏了链式epsilon转移。需要注意的参数是字母表列表和起始状态编号做验证时改成自己题目里的实际字母表和初始状态。这比对着答案重算一遍要可靠得多因为脚本不会犯手算的“少算一层闭包”的错误。做完NFA转DFA后还可以在这段代码后追加Hopcroft最小化算法写法上先按终态分组再迭代切分直到稳定对比答案中的最小化结果。3.2 一个递归下降脚本验证FIRST集和FOLLOW集FIRST集和FOLLOW集的求法看起来简单但一遇到有ε产生式的文法就容易漏算。写一个脚本来算这两个集合可以在考前把教材所有例题全部跑一遍确认自己的手算套路是否完整。# 文法表达方式: {E: [[T, E], [T]], E: [[, T, E], [ε]]} grammar { E: [[T, E]], E: [[, T, E], [ε]], T: [[F, T]], T: [[*, F, T], [ε]], F: [[(, E, )], [id]], } terminals {, *, (, ), id} # ε不算终结符 nonterminals set(grammar.keys()) first {nt: set() for nt in nonterminals} def compute_first(nt): 递归计算非终结符的FIRST集含处理ε产生式 for production in grammar[nt]: for symbol in production: if symbol ε: first[nt].add(ε) break elif symbol in terminals: first[nt].add(symbol) break else: compute_first(symbol) # 确保子非终结符先算 first[nt] | (first[symbol] - {ε}) if ε not in first[symbol]: break # 如果整个产生式所有符号都能推出ε则FIRST(nt)中加入ε return first[nt] for nt in nonterminals: compute_first(nt) print(FIRST集:, {k: sorted(v) for k, v in first.items()})这个脚本的关键是对“如果某个符号能推出ε就要继续看下一个符号”的处理。它的做法是循环里遇到能推出ε的非终结符时不break继续取下一个符号直到遇到终结符或不能推出ε的非终结符才停止。如果整个过程走完没有break就在FIRST中加入ε。手算时最容易漏的就是这一步看到 A → B C 而 B 能推出ε时忘记把 C 的FIRST也并进来。在执行时把grammar和terminals改成自己的文法即可脚本会自动按依赖顺序计算。注意这里有递归调用所以文法的推导层级不要写太深导致递归深度不够理论上大学教材里的文法都不会超过10层Python默认递归限制1000次足够。FOLLOW集的算法类似但要额外处理三条规则其中“A → αB 或 A → αBβ 且 β 能推ε时FOLLOW(B) 包含 FOLLOW(A)”是必须写到代码里的传播逻辑少了这一条结果就会偏小。跑完以后拿标准答案对比如果脚算不一致定位方法是打印每个非终结符的FIRST序列看出现在哪个非终结符上通常在带ε的产生式链上。3.3 用三地址码做基本块划分与DAG重建中间代码和优化章节的大题最实用的复现方式是把题目里的三地址码贴进脚本划分基本块再重建DAG。这比手画要快也能避免在大题上留下低级的“漏边”错误。# 三地址码序列题目原样给出 codes [ (t1, d, -, e), (t2, c, *, t1), (t3, t2, /, f), (t4, b, , t3), (a, t4, , ), # 赋值语句 (t5, d, -, e), # 和t1公共子表达式 (t6, c, *, t5), (b, t6, , ), ] leaders [] # 基本块入口指令编号 leaders.append(0) # 第一条指令是入口 for i, code in enumerate(codes): # 无条件跳转或条件跳转的下一条指令是新的基本块入口 if code[1] in (j, jz, jnz) and i 1 len(codes): leaders.append(i 1) blocks [] current [] for i, code in enumerate(codes): current.append(code) if i 1 in leaders or i len(codes) - 1: blocks.append(current) current [] # DAG节点表记录变量与表达式的对应关系 dag {} for block in blocks: for op, arg1, arg2, result in block: if op : dag[result] arg1 else: node_key (op, arg1, arg2) # 公共子表达式检测相同运算且操作数相同则复用节点 if node_key in dag.values(): result_node [k for k, v in dag.items() if v node_key][0] dag[result] result_node else: dag[result] node_key print(基本块划分:, blocks) print(DAG映射:, dag)脚本的输出会明确地告诉你在哪个基本块中检测到了公共子表达式。上面这段代码里 t1 和 t5 的计算都是 d - eDAG映射会把它们指向同一个节点。这个检测规则就是“相同运算符、相同左操作数、相同右操作数”。写在答案里的DAG图只要和这个映射一致说明公共子表达式没漏。需要注意一下“”赋值的处理逻辑如果右侧是常量或变量它会被标记成父节点引用不用新建运算节点。但多轮赋值同一个变量时后面的赋值会覆盖前面的节点引用这对应了DAG中变量名绑定的处理。答案中如果考察的是改动后的DAG看到同名变量不同引用是正常的别误以为是自己算错了。这一章的核心不在代码量而在公共子表达式的识别上对照答案时重点看这一类复用节点是否找全。4. 避坑实务五个反复出现的复习误区和答题失分点带答案的真题卷用不好比不用还糟。这些年我见过了太多在这类材料上翻车的案例集中体现在五个问题上。每一条都是先描述现象再给出原因分析和解决方案。4.1 只看答案不动手答案成了“眼会了”的安慰剂现象是很多同学拿到带答案的卷子后直接翻答案看完觉得“哦原来如此”合上卷子再做一遍依然卡在同样位置。原因是看答案激活的是识别记忆不是提取记忆真正上考场需要的是提取能力。解决办法是必须把答案遮住按正常考试时间做完再对答案。一个可执行的节奏是每道大题限时15到20分钟做完立即对答案错了的当场在题目旁边标注错因概念错误、算法遗漏、计算失误整卷刷完后统计错因分布是概念错误多就去翻对应章节是计算失误多就加大刷题量。4.2 混淆“语法树”和“语法分析树”一个名字带出的动作差这两种树在编译原理里经常被混着叫但考试的画法完全不同。语法分析树是从文法推导的角度画的节点是非终结符叶子是终结符而抽象语法树AST去掉了只起推导作用的多余节点运算符直接成为根节点。答案中如果画的是AST而你在卷面上画的是完整推导树步骤分可能拿到但最后的节点数对不上。解决办法是做题前先看题目要求是“画出推导过程”还是“构造语法树”后者通常默认是AST如果题目确实没有说明按教材约定画多数学校用的是AST简化画法。4.3 活前缀与LR项目集闭包在最容易“默认显然”的地方丢分现象是在LR分析的大题里学生算到活前缀和项目集闭包时经常省略“该状态的所有活前缀”这一步直接从某一个项目跳到结论。原因是闭包运算本身看起来太机械手算时容易跳过“把所有形如 B → .γ 的项目加入”的动作结果项目集少了条目后面的GO函数和SLR分析表跟着错。解决办法是用固定格式写过程每个项目集先写“闭包前”再写“闭包后”闭包前的初始项目用从上一个状态转移过来的那个项目开头然后逐条检查哪些非终结符的前面有点把它们的产生式全部加入。一定要在草稿纸上展开写几遍跳过任何一层闭包都有机会出错。4.4 把“由答案测试”当成“由测试发现薄弱点”的驱动逻辑带答案真题卷最容易养成的坏习惯是把答案当成工具书不会做了就翻答案最后产生一种“我已经会了这个知识点”的错觉。正确用法是答案只在两种情况下看一是整张卷子做完后集中对照二是同一道题重做两遍以上仍卡住时打开。第一遍做题标出不会的题最后统一对答案。对完答案后不急着看解析先自己按标准答案的分数维度给自己批改比如LL(1)分析表占6分自己填对了几格就给自己几分。这样做可以精确暴露自己到底是“完全不懂”还是“部分遗忘”后者只需要翻课本补两三页前者才需要重新看算法步骤。4.5 忽略专业术语的标准化书写NFA的ε边画成箭头带圈是常见扣分点现象是部分同学用自己发明的符号体系写答案比如把ε标成空串圆圈旁边画个e把FOLLOW集写成FOLLOW把LR(0)项集写成“[E→E.T]”少一个逗号。原因是没有意识到编译原理的改卷是看符号规范性的。解决办法是考前把答案中所有出现的符号术语抄一遍重点包括ε、FIRST、FOLLOW、|、·项目集里的点号、⇒推导符号、├归约符号。字符串的箭头推导常用⇒而在文法产生式中用 →不要在卷面上混用这属于改卷老师一眼就能看到的不规范。5. 把一份真题卷变成十份两遍刷题法与错题反推知识点清单拿到一份带答案的《编译原理》期末试题把它做一遍再对一遍答案是最低效的用法。我通常会建议用两遍刷题法把这份卷子的价值榨干具体做法是第一遍按考试时间完整做对答案只对结果不对过程一周之后第二遍只做第一遍出错的大题做完后再对整份答案的解析过程重点看不一致的过程步。判断标准是“过程步是否与答案给出的推导序列完全一致”如果过程中间某一步的跳转方式和答案不同但结果一致这不叫对叫碰巧需要按答案的标准写法再做一遍。两遍之间如果隔得够久上次对答案时留下的短期记忆已经消退第二遍反应出的才是真正掌握的部分。两遍刷完之后把错题对应的知识点做成一张反推清单格式是一个三列表格题目编号、知识点、概念乱点。比如某道题考的是“消除左递归”概念乱点写“为什么要先提取左公因子再消除左递归顺序反了会怎样”。这张表的价值在于它直接指向你个人的薄弱章节比按教材目录复习高效得多。我做这份事时踩过最大的坑是第一遍做完错了8道题第二遍做了前7道依然错最后发现错的根本原因是把“SLR(1)的展望符号只看FOLLOW集”理解成了“LR(1)的需要逐项手工指定向前搜索符号”。概念混淆是错题库里最隐蔽的一种因为看起来每次错的点都不一样其实是同一个概念基础没搭牢。还有一个很实用的习惯在考前最后36小时里不看答案纸只看你自己写的那张错题清单逐条在脑海里演算对应题目的过程。演算不出来的翻课本的算法部分不用再翻整份答案。这个习惯连续用三份真题卷后知识点的复盘时间可以从一个下午压缩到45分钟。做真题卷不只是为了预测考题更是为了校准自己的复习方向。希望这份材料能帮你提前排掉那些会在考场上拽住你的雷祝期末顺利。本文还有配套的精品资源点击获取