编译原理试卷A考点剖析:从词法分析到代码生成的完整复习链路
简介常州工学院《编译原理》试卷A是一份面向计算机专业学生的期末试题覆盖词法分析、语法分析、中间代码生成等编译流程核心知识点既适合考前自测、查漏补缺也可供教师布置作业或命题时参考。资源为单个doc文档体积仅55KB内页排版清晰可直接打印练习。试卷共五个大题依次涉及正规表达式与最简DFA构造、逆波兰表示转换、文法二义性证明与语言描述、First集与Follow集计算及LL(1)预测分析表构建、if语句四元式序列翻译并在题目标注了分值便于规划答题时间。已有391人浏览学习属于“试卷库”中的常用备考资料。通过完整作答这些题目能系统梳理编译原理主干内容理解词法规则、语法判定和中间代码生成之间的衔接熟悉期末常见题型与解题方法。1. 一份编译原理试卷A到底在考什么它不是题库是整条编译链路的地图拿到《编译原理试卷A》的时候很多人第一反应是找原题、背题型。但真到了考场上你会发现丢分的地方从来不是没背过而是没看懂这张卷子背后的脉络。编译原理这门课和别的课不一样它把“字符串怎么变成机器码”这条完整链路切成了词法、语法、语义、中间代码、代码生成五段而一份成型试卷的考点分布恰恰就是这条链路的等比例压缩。只要抓住“输入串到目标代码”这条主线题目再怎么换皮考点都是那几个固定的节点。这份试卷适合三类人一是正在准备期末考试的在校生想用一套真题摸清重点二是准备考研或面试的工程师需要快速梳理编译基础三是工作中偶尔和解析器、DSL、代码生成打交道想补上理论短板的开发者。读这一篇不是要你把题目背下来而是带着你从卷面倒推知识结构再讲清楚每类题型的展开方式和踩坑点。2. 词法与语法题从正则到LR分析表的四类必练题型2.1 正则表达式与自动机题NFA、DFA、最小化步骤别跳词法分析在试卷A里通常占12到18分最常见的组合是“给一个正则表达式写出NFA再转成DFA并最小化”。这道题看着绕但其实是全卷性价比最高的题目因为套路完全固定练熟之后几乎不会丢分。我把它拆成三步操作第一步画NFA。拿到正则表达式后先按运算符的优先级加括号把“|”“*”“连接”拆成子表达式。然后每个子表达式画一个片段字符用一条边连接用串接闭包用带回边的状态并集用两个分支合并到一个起点和一个终点。这步的关键是不要试图一步到位画一个“看起来很简洁”的NFA宁可多画两个空转移边为了后续算法服务。第二步对NFA做子集构造得到DFA。从NFA的初态出发对每个输入符号求ε-闭包得到一组状态子集。每个子集就是DFA的一个状态。这个计算过程我建议老老实实写状态子集表格列是输入符号行是新生成的状态集直到没有新状态出现。真正容易出事的地方在ε-闭包漏算空转移边形成的环必须完整展开否则后面对输入串的判定一定会错。第三步DFA最小化。先把终态和非终态分成两个集合然后不断按输入符号分裂——只要同一个集合内的状态对某个符号走到了不同集合就继续分裂直到不动点。这步的细节是如果某个状态对符号没有出边不要直接丢弃要把它视为“走入死状态”在最小化时和死状态一起归类否则最终得到的状态数会比正确答案少。下面给出一个我用来自检的最小化流程写成伪代码级别的步骤输入DFA M(Q, Σ, δ, q0, F) 1. 初始化划分 P {F, Q-F} 2. 重复 对 P 中每个集合 G对每个符号 a 若 G 中的状态经 a 到达的集合不完全相同 则按“到达集合的归属”把 G 分裂 直到没有任何集合被分裂 3. 把同一集合内部的状态合并为一个状态 4. 包含 q0 的集合作为新初态包含原终态的集合作为新终态逻辑上这个流程依赖一个事实完全等价的状态必须有完全一致的“未来行为”也就是对任意输入串两者要么同时接受要么同时拒绝。参数上分裂条件只看一步转移后的集合归属所以必须按“划分—精化”反复迭代不能只扫一遍。你可能觉得这很像闭包计算没错它和求可达状态本质是同一类不动点算法。计算完成后用原始正则里的典型串包含空串、重复项、非法串至少各一个去测试新DFA能跑通这题才叫稳了。2.2 LL(1)与LR(1)的取舍判断FIRST/FOLLOW和项目集的两种考法语法分析是试卷的大头通常能到30分以上。它一般分成两个阵营自顶向下考LL(1)自底向上考LR(0)/SLR(1)/LR(1)。很多同学会陷入一个误区——试图把所有文法都套进LR(1)结果项目集规范族画到一半卷面就写不下了。试卷A里显然更偏爱“给出文法判断能否用LL(1)并构造预测分析表”这类题。判断LL(1)需要走一遍标准流程消除左递归。注意直接左递归和间接左递归都要处理。提取左公因子。对每个产生式计算FIRST集。如果某个非终结符能推出空串还要计算FOLLOW集。检查冲突对任意产生式 A → α | β如果 α 和 β 的FIRST集相交或者其中某个FIRST集包含空串而FOLLOW(A)与另一个产生式的FIRST集相交则文法不是LL(1)。FOLLOW集是这题的坑王。初学最容易把“开始的符号”和“跟随的符号”搞混比如把开始符号的FOLLOW默认成空忘了要放入结束标记$。我的习惯是初始化时把所有文法开始符号的FOLLOW集中加入$然后反复套三条规则直到没有新增项。有些时候你不需要手工算可以用一个小脚本校验。我常用下面这个极简FIRST集计算的骨架把这个挂在本机跑一下能快速对照手算结果def first_set(grammar, symbol): # grammar: dict, 非终结符 - 产生式列表, 产生式是字符串列表 # symbol: 当前计算的符号 if symbol not in grammar: # 终结符的FIRST就是自身 return {symbol} result set() for production in grammar[symbol]: if production [epsilon]: # 空产生式直接贡献空串 result.add(epsilon) continue for i, ch in enumerate(production): if ch symbol: continue # 避免左递归死循环实际完整版要维护访问标记 f first_set(grammar, ch) result | (f - {epsilon}) if epsilon not in f: break # 这一项没有空串后续无法继续传导 else: # 整个产生式都能推空才贡献空串 result.add(epsilon) return result这段脚本的逻辑对产生式逐个符号扫描如果某个符号的FIRST集里没有空串就停止扫描因为后面的符号不可能再贡献FIRST了只有当产生式右侧全部都能推出空串整个产生式才向FIRST集增加空串。参数上的关键是epsilon的传导处理实际做课程练习时最好加上“已访问符号”字典防止间接左递归让递归调用无限循环。2.3 算符优先分析与LR分析的边界别混着用有的试卷会把“算符优先分析表”和“LR分析表”放在同一张卷子里而且故意用同一份文法。这时候要特别注意算符优先分析的基础是终结符之间的优先关系它只看终结符不看非终结符LR分析的基础是项目集和状态转移。混用就会出现“等号关系”处理错误——算符优先里表示的归约并不是普通算术里的相等。做题时先判断题目要求的是“移进-归约”分析还是要构造分析表。如果要求构造算符优先表就老老实实计算FIRSTVT、LASTVT再根据产生式形态填关系如果要求LR分析就不要去算优先关系直接写项目集规范族。这个选型判断本身就是考点你把它走对等于白赚一道题的分数。3. 语义分析与中间代码属性文法、三地址码与控制流翻译3.1 属性文法题综合属性与继承属性怎么分配不会乱试卷A在语义分析部分的典型考法是给出一个小型语言的文法问“为文法设计属性写出语义规则并为某语句输出带注释的语法树”。这类题表面的计算量不大但特别容易在属性方向的定义上翻车。属性分成两类综合属性节点的属性值由子节点属性计算得到信息自底向上传递。典型代表是表达式的结果、语句中变量的类型。继承属性节点的属性值由父节点或兄弟节点传递而来信息自顶向下或横向传递。典型代表是语句中希望传递的符号表引用、循环入口标签等。我一般的做题顺序是先把语法树画出来然后从最终需要的结果反推哪一层能直接算出来——能被孩子算出的是综合属性需要从外面传入的是继承属性。比如“声明语句的类型”通常由声明中的类型名向上综合“变量在表达式中的类型检查”则由变量声明处向下继承。把每个非终结符所需的属性写在语法树节点旁再逐条写语义规则就不会漏。里面有一个高频坑赋值语句id : expr里如果希望检查左部和右部类型是否一致左部变量id的类型是从环境继承下来的属性而不是从expr综合上来的。所以常见语义规则写法是S.type : if id.type expr.type then id.type else error。这个规则里id.type用的是继承属性很多人会当成综合属性来写方向一错整个语义检查的传递链就断了。3.2 控制流翻译题回填技术做布尔表达式和if语句的代码布局中间代码考得最多的是三地址码尤其是带布尔表达式的if-else和while语句翻译。这类题最稳妥的做法是使用“回填”思路生成跳转指令时不直接填目标地址先挂一个待填列表等后续确定目标后再回填。手工做题时你不需要完整模拟回填的数据结构但我建议按下面这套顺序展开画出语句的语法分析树。给每个语句节点标记真出口、假出口和下一个语句的入口标签。从左到右生成三地址码遇到跳转指令先空着目标记录这条指令的编号。当后续标签确定时反向把编号填入。以布尔表达式a b or c d为例生成的三地址码一般长这样if a b goto L_true if c d goto L_true goto L_false L_true: ... L_false: ...逻辑上这里用到了短路求值——只要第一个条件成立就不再判断第二个条件。参数上要注意or的语义是“跳过其余条件直接走真出口”所以前两个if的目标都是L_true而goto L_false是两者都不满足时的兜底跳转。如果你手写的答案里把第二个条件写成了if c d goto L_false那一定是把短路语义理解反了。多练几个嵌套组合之后这类题基本就是填模板。3.3 过程调用的三地址码形参实参、返回值和栈上临时量的处理试卷A偶尔会考一段带函数调用的赋值语句比如x : f(a, b)要求写出完整的三地址码。这里很多人漏掉“实参计算”这一层。正确的展开是先把实参表达式算出结果存到临时量再把临时量作为参数传入然后调用函数最后把返回值赋给左部变量。一个容易丢分的点是把参数求值顺序写乱尤其是当实参本身包含副作用的表达式时。如果题目没有特别说明求值顺序我一般按从左到右展开并显式注释“按PLUS语言语义参数从左到右求值”。如果试卷明确说明求值顺序未定义则以表达式树的自然顺序为准。这提醒我们语义分析不只看你会不会生成指令还看你能不能感知语义规则对代码形状的影响。4. 运行时环境与代码生成活动记录、寄存器分配和局部优化题4.1 栈式存储题活动记录与调用序列这几个字段千万别写漏运行环境这块的考点集中在“栈式分配”和“活动记录”。试卷里比较典型的问法是“画出某次递归调用后的运行栈”或者“写出过程调用前后调用序列的步骤”。活动记录的字段一般是局部变量、临时量、保存的机器状态返回地址和旧栈指针、参数、返回值、以及可选的访问链/控制链。画运行栈时我建议按下面的顺序布局从高地址向低地址字段分类内容方向返回结果返回值区域低地址附近实参调用者压入的参数向高地址增长控制链指向调用者活动记录的指针固定位置访问链指向静态外层活动记录的指针嵌套语言才有固定位置保存状态返回地址、旧SP和FP固定位置局部变量被调函数的局部变量再往低地址这里的关键不是背字段而是看懂哪些由调用者压入、哪些由被调者填写。实参、返回地址和旧栈指针一定是调用者负责压栈的局部变量和临时量由被调函数进入后分配。如果你画出的栈里局部变量出现在实参上方基本就能看出调用序列画反了。访问链是最常见的迷惑项如果题目语言不是Pascal式嵌套过程访问链可以不画画了反而画蛇添足。4.2 代码生成与优化题寄存器描述符和变量地址描述符的双向追踪代码生成题的典型考法是用基本块给你一段中间代码让你用寄存器描述符和变量地址描述符模拟寄存器分配。考试时逐行维护两张表很繁琐但它是把“寄存器里的值”和“内存里的值”这个状态追踪清楚的最好手段。表结构大概是这样的寄存器存放的变量是否仍与内存一致R1x一致x仍在栈中R2y不一致y已修改栈中值已过期变量所在位置xR1, 内存(x)yR2z内存(z)代码生成时每个用到变量的指令先去寄存器表查是否命中如果命中且内容有效直接生成寄存器引用指令否则生成内存加载指令。赋值给变量时优先写入寄存器同时更新两张表。到基本块结束需要把仍然只存在于寄存器中的变量保存回内存。很多同学会在“何时写回内存”这里栽跟头。规则很简单如果变量在寄存器里被修改过且后续块还可能读取它那么在本块结尾必须保存如果本块内它是最后使用后面不再引用就不必保存。做题时只要记住“寄存器只是缓存内存才是最终归宿”就不会漏掉store指令。4.3 循环优化三件套代码外提、强度削减和归纳变量删除试卷的优化大题如果出到“对下列循环进行优化”基本是在考这三件事。我给出一段极简的中间代码来演示一下L: i : 0 t1 : 4 * i t2 : arr[t1] if t2 100 goto L_end i : i 1 goto L L_end:第一步代码外提循环体内如果出现“不随迭代改变”的表达式把它移到循环前。上面的4*i其实随i变化不能外提。要找一个真正不依赖循环变量的比如c : 2 * p这种其中p在循环内不变。外提的判断标准是表达式里每个操作数在循环内所有赋值点之后都没有被重新定义。第二步强度削减4 * i这种乘法可以用每次递增4的加法替代。引入一个临时变量比如让t 4 * i在循环入口初始化循环体内每次t : t 4。这样乘法变成加法指令代价明显下降。第三步归纳变量删除如果i只在循环计数和数组寻址时使用而且t完全由i推导而来那i可能被删除直接用t做循环判断。这一步要谨慎删除的前提是i在循环外没有被引用否则你删掉的不是一个变量而是一个对外接口。优化题的坑在于顺序。考试时很多人先做归纳变量删除再做强度削减结果两边互相干扰。我的习惯是先外提、再削减、最后删归纳变量这个顺序和你实际做优化的编译器流程也一致。5. 从试卷A的常见丢分点聊开去五个踩坑记录5.1 FOLLOW集合算出来的结果比标准答案少了$现象手算FOLLOW(开始符号)时只写了过程体里的跟随符号把结束标记丢了或者用上面那段Python脚本时发现结果集和预期不一致。原因FOLLOW集的定义要求开始符号的FOLLOW元素包含输入结束符。多数教材把这条作为初始化规则而不作为推导规则如果自主计算时只套用产生式推导就会漏掉$。解决算FOLLOW前先写好三类初始化——开始符号加$对形如A → αB的产生式把FOLLOW(A)全部并入FOLLOW(B)对A → αBβ的产生式把FIRST(β)中除空串外的符号并入FOLLOW(B)若FIRST(β)含空串则还要并入FOLLOW(A)。把这个流程写成检查清单逐条核对。5.2 LR项目集规范族出现凭空多出来的搜索符现象构造LR(1)项目集时某些项目的搜索符来自初始项目传递但闭包过程中忽略了上下文导致多加了搜索符最终SLR(1)和LR(1)分析表混在一起。原因很多教材在引入LR(1)时会强调“搜索符比SLR更精确”于是有人把SLR的FOLLOW集直接搬到LR(1)的搜索符上。但LR(1)搜索符来自产生式右侧的FIRST计算不是FOLLOW全集。二者在简单文法上结果可能一样在复杂文法上就会不同。解决做LR(1)闭包时用“当前项目后紧跟的符号串的FIRST”作为新项目的搜索符不要直接用FOLLOW。简单校验方法分析表的ACTION表里如果某个状态对同一个终结符同时出现移进和归约说明这里很可能混用了SLR的风格。考试时如果时间紧张宁愿先做SLR(1)也别为了追求LR(1)把搜索符写成一锅粥。5.3 属性文法里的类型信息方向写反现象语义规则写了S.type : id.type结果题目给的文法里id是在产生式左侧定义的明明应该从环境继承类型却写成了子节点向父节点传值。原因想当然地把所有属性都当成综合属性。实际上符号表里的变量类型、循环深度、嵌套层级这些信息都是从上下文环境获取的天然是继承属性。只有表达式结果、语句生成代码序列这类由自身计算出来的值才是综合属性。解决写上每个属性之前先问自己这个值是从哪里知道的如果它来自“声明处”或“外面传入的环境”就是继承属性如果它来自“子节点算完给我的”才是综合属性。考试时拿不准就在属性名前写inh或syn让阅卷老师看到你的意识至少不会整个方向都废掉。5.4 中间代码生成时跳转目标乱填回填之后对不上现象写if语句的三地址码goto后面先填了一个占位编号后来回填时发现编号少于实际条数跳到了错误位置。原因手工做题时还没有确定生成顺序就先写跳转导致指令条数变动后编号对不上。解决我建议按“先框架后填充”的顺序——先写出带空跳转的完整指令列表用L_true、L_false这类符号代替实际编号而不是真的先写数字。全部框架落定后再为每个标签分配唯一编号并替换。这样即使中间插入若干临时量指令跳转目标也不会错位。这个习惯在考场上能省下大量改错时间。5.5 代码生成的最终指令忘记保存寄存器中的变量现象基本块末尾生成的汇编指令里只有计算指令没有store指令导致结果在后续基本块读不到。原因模拟寄存器分配时只关注了当前指令忽略了“基本块边界是寄存器状态的同步点”这一原则。解决每次一个基本块结束时把所有“寄存器中有且已修改但内存未更新”的变量全部生成store指令然后清空寄存器描述符。这一步做完再进下一个基本块状态干净不会带着上一个块的脏数据进入新块。把这个“基本块结尾清脏”的动作作为一个默认步骤所有代码生成题都不会漏。6. 用试卷A做复习闭环把错题变成一张可复用的考点映射表这可能是整篇笔记里对你最有用的一个技巧。拿到试卷A别急着整套做也别急着对答案而是先给它做一次考点拆解每道题对应到知识模块和具体算法。做完后按下面这个模板整理题号对应考点涉及算法/表格错误类型1词法分析NFA→DFA→最小化漏ε闭包2语法分析LL(1)预测分析表FIRST/FOLLOW算错3语义分析属性文法继承属性方向反4中间代码回填技术跳转目标错位5代码优化循环优化三件套外提前置条件漏判把所有错题往里填一个星期后你会得到一张属于你自己的漏洞分布图。这个方法的厉害之处在于它不针对某一次考试而让你看清自己容易在哪个环节翻车。这套方法的来源说起来也简单。我认识一位已毕业的同行当年复习时把近三年的卷子全部按上面的方式做映射结果发现他所有的丢分几乎都集中在“继承属性和LR(1)搜索符”两个点上。第二次复习时直接把这两个点作为主攻方向最终成绩显著提升。后来我带新人学编译时也一直用这个思路——不管手头拿的是哪一份试卷先不要做先拆结构把考点归位等结构清楚了再做题事半功倍。如果你想偷懒一点还有一个更省力的做法把卷子里的中大型题比如LR分析、三地址码生成、循环优化单独抽出来不按原卷顺序做而是按“词法→语法→语义→优化”的流程重排从前往后自己当老师讲一遍。能流畅讲出来就说明你已经站在出题人的角度思考问题了。考试时遇到改编变体也不会慌因为你掌握的是框架不是答案模板。这是我最想分享的一个备考习惯也是我自己的血泪经验——别把试卷当题库把它当训练集。训练集的价值不在于刷完而在于每次迭代都在修正模型参数。希望帮到你。本文还有配套的精品资源点击获取