ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

编译原理核心:从词法分析到语法分析,掌握预测分析表与LR分析

2026/8/14 9:01:01 拓冰建站 浏览量
编译原理核心:从词法分析到语法分析,掌握预测分析表与LR分析

1. 从“天书”到“通关秘籍”:我的编译原理期末复习心路

又到了学期末,编译原理这门课的名字一出现,估计不少同学已经开始头疼了。我记得自己当年第一次翻开那本厚厚的“龙书”,看着满篇的“文法”、“自动机”、“语法制导翻译”,感觉就像在看天书。这门课的理论性强、概念抽象,前后章节环环相扣,如果前面没搞懂,后面基本就是听天书。但恰恰是这门“硬核”课程,是理解计算机如何“理解”我们写的代码的基石,无论是想深入做编译器、解释器,还是想提升自己debug和设计领域特定语言(DSL)的能力,编译原理都是绕不开的一环。

期末复习,绝不是把书从头到尾再翻一遍,那效率太低,也抓不住重点。有效的复习,更像是在你已经搭建的知识框架上,进行加固、连接和查漏补缺。你需要把散落的概念串成线、织成网,最终形成一套能应对各种考题(无论是计算、分析还是设计)的“肌肉记忆”。这篇文章,我就结合自己当年备考和后来工作中反复应用的经验,聊聊如何高效地进行编译原理期末总复习,目标是帮你从“知道”变成“会用”,从“畏惧”变成“通关”。

2. 构建知识地图:理解编译的“流水线”

复习的第一步,不是埋头做题,而是站起来,俯瞰整个编译过程的全貌。你得清楚编译器这个“黑盒”里,到底有几道工序,每道工序输入什么、输出什么、核心任务是什么。这能让你在遇到具体题目时,迅速定位它属于哪个阶段,该调用哪部分知识。

2.1 编译的六大阶段与核心产出

我们可以把编译器想象成一个精密的加工流水线,源代码是原材料,目标代码是成品。这条流水线通常分为六个核心阶段:

  1. 词法分析:这是第一道工序,像个扫描仪。输入是源代码字符串,输出是一串记号。它的核心任务是根据正则表达式定义的词法规则,识别出一个个最小的语法单元,比如关键字(if,while)、标识符(变量名count)、运算符(+,=)、常数等,同时过滤掉空格、注释等无关字符。这里的关键是掌握如何设计正则表达式,以及理解有限自动机如何实现这种识别。考题常给一小段代码,让你写出识别出的记号序列。

  2. 语法分析:这是第二道工序,像个质检员,检查结构是否符合规范。输入是记号流,输出是语法树。它的核心任务是依据上下文无关文法,检查记号流是否符合语言的语法结构。这里你会遇到各种文法概念(最左推导、最右推导、二义性),以及两大类分析方法:自顶向下(如LL(1)分析)和自底向上(如LR分析)。这是编译原理的重中之重,也是考题最集中的部分。

  3. 语义分析:工序继续,像个逻辑检查员。输入是语法树,输出是带标注的语法树。它的核心任务是进行上下文相关性质的检查,比如类型检查(整数能不能赋值给字符串?)、作用域分析(这个变量在这里能用吗?)、控制流检查break语句是否在循环内?)。它不产生新的中间表示,而是给语法树附加属性信息。

  4. 中间代码生成:从这里开始,进入“优化区”。输入是带标注的语法树,输出是中间表示。常见的中间表示有三地址码、四元式、P-代码等。它的核心任务是将与机器无关的语法树,转换成一种更简单、更易于后续分析和优化的抽象指令形式。例如,一个复杂的表达式a = b + c * d可能会被转换成几条三地址码。

  5. 代码优化:这是“精加工”环节。输入是中间代码,输出是优化后的中间代码。它的目标是在不改变程序语义的前提下,提高目标代码的运行效率或减小其体积。常见的优化包括:常量传播、公共子表达式消除、死代码删除、循环优化等。这部分在考试中可能以分析题或简答题形式出现,要求你识别可优化的代码段或描述某种优化技术。

  6. 目标代码生成:最后一道工序,包装出厂。输入是优化后的中间代码,输出是目标机器代码(汇编或机器码)。它的核心任务包括指令选择(选哪条机器指令来实现中间代码操作)、寄存器分配(有限的寄存器给谁用?这是个大难题)、指令调度(调整指令顺序以利用CPU流水线)。这部分通常不是本科考试的重点,但需要了解基本流程。

把这六个阶段的名字、顺序、输入输出和核心任务背下来,是基础中的基础。更关键的是,要在脑子里形成一条清晰的“数据流”:源代码 -> 记号流 -> 语法树 -> 带标注的语法树 -> 中间代码 -> 优化后中间代码 -> 目标代码。

2.2 各阶段的“武器库”:核心概念与工具

每个阶段都有其专属的理论“武器”。复习时,你需要把这些武器和对应的阶段牢牢绑定:

  • 词法分析:武器是正则表达式有限自动机。要能互相转换:给定正则表达式能画出NFA(非确定有限自动机),懂得子集构造法将NFA确定化为DFA(确定有限自动机),并能用Hopcroft算法对DFA进行最小化。考题常是:“为某语言成分设计正则表达式并构造其DFA”。
  • 语法分析:武器是上下文无关文法。这是核心战场。你需要掌握:
    • 文法改造:消除左递归、提取左公因子,这是进行LL(1)分析的前提。
    • FIRST集和FOLLOW集的计算:这是LL(1)文法的判定和预测分析表构造的基石。必须熟练到形成条件反射。
    • LL(1)分析:掌握预测分析表的构造方法,能模拟分析过程。这是自顶向下分析的典型代表。
    • LR分析:理解活前缀、LR(0)项目、项目集规范族、SLR(1)、LR(1)、LALR(1)分析表的构造思想。虽然构造完整的LR分析表非常繁琐,考试通常只考到构造识别活前缀的DFA(即项目集规范族),或者给一个简单的SLR(1)分析表让你进行移进-归约分析。关键要理解“移进”和“归约”动作的含义。
  • 语法制导翻译:这是连接语法分析和中间代码生成的桥梁。武器是属性文法(综合属性和继承属性)和翻译方案。要能根据给定的语法制导定义,为语法分析过程中的每个产生式附加语义动作,从而在构造语法树的同时计算出所需的属性(如类型、代码地址等)。

当你看到一个题目,能立刻反应出它属于哪个阶段、需要用哪个工具解决,你的复习就成功了一半。

3. 攻克核心堡垒:语法分析与预测分析表

语法分析无疑是编译原理期末考的核心堡垒,而预测分析表的构造与使用,又是这座堡垒的钥匙。很多同学卡在这里,因为涉及的计算(FIRST、FOLLOW集)和判断(LL(1)条件)比较繁琐。我们把它拆开揉碎了讲。

3.1 为什么需要预测分析表?

自顶向下分析(如递归下降、LL分析)面临一个根本问题:当面对一个非终结符和当前的输入记号时,我该用它的哪个产生式(候选式)进行推导?预测分析表就是一个“决策表”,行是非终结符,列是终结符(包括结束符$),表格内的内容指明了应该选用哪个产生式,或者报错。

3.2 构造预测分析表的四步法

构造过程是机械的,但必须理解每一步的逻辑。我们通过一个经典例子来贯穿始终。假设有文法G:

  1. E -> T E'
  2. E' -> + T E' | ε
  3. T -> F T'
  4. T' -> * F T' | ε
  5. F -> ( E ) | id

这是一个消除了左递归、提取了左公因子的表达式文法,它就是一个LL(1)文法。

第一步:计算每个文法符号的FIRST集

FIRST(α) 定义为:能从α推导出的所有串的第一个终结符的集合。如果α能推出ε,则ε也在FIRST(α)中。

  • 计算技巧:从终结符开始,终结符的FIRST集就是它自身。然后从产生式右侧不断向前看。
    • FIRST(id) = {id},FIRST('(') = {'('},FIRST(')') = {')'},FIRST('+') = {'+'},FIRST('*') = {'*'}
    • FIRST(F):看产生式F -> ( E ) | id。右侧第一个符号分别是(id,都是终结符,所以FIRST(F) = { '(', id }
    • FIRST(T'):看产生式T' -> * F T' | ε。一个候选式以*开头,另一个是ε。所以FIRST(T') = { '*', ε }
    • FIRST(T):看产生式T -> F T'。右侧以F开头,所以FIRST(T)包含FIRST(F)中所有非ε的元素,即{ '(', id }。因为FIRST(F)不含ε,所以计算停止。FIRST(T) = { '(', id }
    • FIRST(E'):同理,FIRST(E') = { '+', ε }
    • FIRST(E)E -> T E',右侧以T开头,所以FIRST(E)包含FIRST(T)中所有非ε元素,即{ '(', id }

第二步:计算每个非终结符的FOLLOW集

FOLLOW(A) 定义为:在所有句型中,紧跟在非终结符A后面的终结符的集合。如果A是某个句型的最后一个符号,那么结束符$也在FOLLOW(A)中。

  • 计算规则
    1. $放入开始符号的FOLLOW集中(这里是FOLLOW(E))。
    2. 如果存在产生式B -> α A β,那么将FIRST(β)中除ε外的所有元素加入FOLLOW(A)
    3. 如果存在产生式B -> α A,或者B -> α A βε ∈ FIRST(β),那么将FOLLOW(B)的全部加入FOLLOW(A)
  • 迭代计算:这是一个需要反复迭代直到所有FOLLOW集不再变化的过程。
    • 初始化:FOLLOW(E) = { $ },其他为空。
    • 看产生式1:E -> T E'。这属于B->αAβ形式,其中A=T,β=E'。所以将FIRST(E')中除ε外的元素加入FOLLOW(T)FIRST(E'){+, ε},所以将+加入FOLLOW(T)。同时,因为E是左部,T E'是右部,且E'T后面,还符合规则3:B=E, A=T, β=E'ε ∈ FIRST(E'),所以还要把FOLLOW(E)加入FOLLOW(T)。目前FOLLOW(E)={$},所以FOLLOW(T)现在有{+, $}
    • 看产生式2:E' -> + T E'。这属于B->αAβA=T,β=E'。将FIRST(E')中除ε外的元素(即+)加入FOLLOW(T)FOLLOW(T)变为{+, $}+已存在)。同时,因为β=E'能推出ε,还要把FOLLOW(E')加入FOLLOW(T)。但FOLLOW(E')现在还不知道,先记下这个依赖关系。
    • 继续分析所有产生式,并反复迭代,最终可以得到:
      • FOLLOW(E) = { $, ) }// 因为F -> ( E ))跟在E后面
      • FOLLOW(E') = FOLLOW(E) = { $, ) }// 因为E -> T E'E'在最后,继承E的FOLLOW集
      • FOLLOW(T) = { +, $, ) }// 来自产生式1和2的分析
      • FOLLOW(T') = FOLLOW(T) = { +, $, ) }// 因为T -> F T'
      • FOLLOW(F) = { *, +, $, ) }// 来自产生式T' -> * F T'T -> F T'的分析

第三步:根据FIRST和FOLLOW集填充预测分析表

对于文法中的每个产生式A -> α

  1. 对于FIRST(α)中的每个终结符a,将A -> α填入表项M[A, a]
  2. 如果ε ∈ FIRST(α),那么对于FOLLOW(A)中的每个终结符b(包括$),将A -> α填入表项M[A, b]

应用到这个文法:

  • E -> T E'FIRST(T E') = FIRST(T) = { '(', id }。所以在E行,(id列填入此产生式。
  • E' -> + T E'FIRST(+ T E') = { + }。在E'行,+列填入此产生式。
  • E' -> ε:因为ε ∈ FIRST(ε),所以对于FOLLOW(E') = { $, ) }中的每个终结符,在E'行的$)列填入E' -> ε
  • ... 以此类推填充完所有产生式。

最终得到的预测分析表如下(空表示报错):

非终结符id+*()$
EE -> T E'E -> T E'
E'E' -> + T E'E' -> εE' -> ε
TT -> F T'T -> F T'
T'T' -> εT' -> * F T'T' -> εT' -> ε
FF -> idF -> ( E )

第四步:使用预测分析表进行语法分析

有了这张表,分析过程就变成了一个机械的查表过程。你需要一个(存放待匹配的文法符号)、一个输入缓冲区(存放剩余的输入串,以$结尾),以及一个输出流(记录使用的产生式)。

分析id + id * id的过程简述如下:

  1. 初始化:栈底为$,栈顶为开始符号E。输入缓冲区为id + id * id $
  2. 栈顶是E,当前输入是id。查表M[E, id],得到E -> T E'。将E弹出,将T E'逆序压栈(保证最左推导)。输出该产生式。
  3. 栈顶变为T,输入仍是id。查M[T, id],得T -> F T'。弹出T,压入T' F(逆序)。输出。
  4. 栈顶为F,输入id。查M[F, id],得F -> id。弹出F,压入id。输出。
  5. 栈顶为id,输入也是id,匹配。弹出栈顶id,输入指针后移到+
  6. 栈顶变为T',输入是+。查M[T', +],得T' -> ε。弹出T',不压入任何东西(因为ε)。输出。
  7. 栈顶变为E',输入是+。查M[E', +],得E' -> + T E'。弹出E',压入E' T +(逆序)。输出。
  8. 栈顶为+,输入为+,匹配。弹出+,输入指针后移到id
  9. ... 如此继续,直到栈和输入都只剩下$,分析成功。

这个过程清晰地展示了如何根据当前栈顶和输入符号,唯一地确定下一步动作,这正是LL(1)文法的“预测”能力所在。考试中,你很可能需要完整地构造这样一张表,或者根据已有的表模拟分析过程。

4. 实战演练与高频考点拆解

理解了核心原理,就需要通过实战来巩固。期末考试的题型通常比较固定,抓住以下几类高频考点进行针对性练习,能事半功倍。

4.1 题型一:文法设计与改造

这类题目通常给出一段自然语言描述,要求你设计出相应的上下文无关文法。例如:“设计一个文法,能生成所有配对括号的字符串,如(),(()),()(())等。”

解题思路

  1. 确定核心递归结构:配对括号的本质是嵌套或并列。我们可以定义一个非终结符S表示一个“配对括号单元”。
  2. 写出基础产生式:最基础的情况是空串和一对括号:S -> ε | ( S )。这个文法能生成(),(()),((()))等嵌套结构。
  3. 补充并列结构:要生成并列的()(),需要允许S的并列连接。可以修改为:S -> ε | ( S ) S。这个文法就能同时描述嵌套和并列了。
  4. 检查二义性:思考字符串()()是否有两种不同的语法树?在这个文法下,S -> (S)S -> ()S -> ()(S)S -> ()()S -> ()()的推导是唯一的。通常这类简单文法不会在本科考试中涉及复杂二义性。

关键技巧:设计文法时,先从最简单的、不可再分的情况写起,然后思考如何用递归(自引用)来描述更复杂的情况。写完务必用几个典型例子(最短的、嵌套的、并列的)去验证。

4.2 题型二:计算FIRST、FOLLOW集与判断LL(1)

这是必考题。给你一个文法,要求计算所有非终结符的FIRST和FOLLOW集,并判断它是否是LL(1)文法。

解题步骤与避坑点

  1. 先消除左递归和提取左公因子:这是前提!一个存在左递归或公共左因子的文法肯定不是LL(1)。题目给的文法可能已经处理过,也可能需要你先处理。
  2. 系统化计算FIRST集
    • 准备一张表格,列出所有文法符号(终结符和非终结符)。
    • 终结符的FIRST集就是它自己,先填好。
    • 从左到右扫描每个产生式,根据规则计算非终结符的FIRST集。这是一个迭代过程,可能需要多轮扫描直到所有集合不再变化。建议用铅笔轻写,方便修改。
    • 常见错误:忽略ε。当某个候选式能推出ε时,意味着在计算其他符号的FIRST集时,需要“跳过”它继续看后面的符号。
  3. 系统化计算FOLLOW集
    • 初始化:开始符号的FOLLOW集加入$
    • 仔细应用三条规则,特别是规则3(继承FOLLOW集)最容易遗漏。必须反复迭代,直到一整轮下来所有FOLLOW集都没有新增元素为止。
    • 常见错误:忘记$;在处理A -> αBβ时,如果β能推出ε,忘了把FOLLOW(A)加入FOLLOW(B)
  4. 判断LL(1):对于文法的每一个非终结符A,它的任何两个不同的产生式A -> αA -> β,必须满足以下条件:
    • FIRST(α) ∩ FIRST(β) = ∅
    • 如果ε ∈ FIRST(β),那么FIRST(α) ∩ FOLLOW(A) = ∅。(对α也同理)
    • 简单说:就是根据当前输入符号,能唯一确定选哪个产生式。检查方法就是看上面构造的预测分析表每个格子是否最多只有一个产生式。如果同一个格子出现了两个产生式,就不是LL(1)。

4.3 题型三:LR分析项目集与活前缀DFA

对于自底向上的LR分析,考试难点往往在构造识别活前缀的DFA(即LR(0)或SLR(1)的项目集规范族)。

核心概念

  • 项目:在产生式右部某处加一个点“·”,表示分析进度。如A -> α·β表示α已识别,期待β。
  • 项目集闭包:如果项目A -> α·Bβ在集合中,且B -> γ是一个产生式,那么B -> ·γ也应该加入该集合。这代表了“期待B时,就要开始准备识别B的产生式”。
  • GO函数(状态转移):给定一个项目集I和一个文法符号X,GO(I, X) 是从I中所有形如A -> α·Xβ的项目,通过将点移过X,得到新项目A -> αX·β,然后求其闭包所构成的集合。

构造DFA的步骤

  1. 构造初始项目集I0:它是S' -> ·S的闭包(S'是增广文法的开始符号,S是原文法开始符号)。
  2. 对于每个项目集I,和每个文法符号X(终结符或非终结符),计算 GO(I, X)。如果结果非空且是一个新集合,就将其作为一个新状态,并添加一条从I到新状态的标记为X的边。
  3. 重复步骤2,直到没有新状态产生。

避坑经验

  • 一定要先构造增广文法:在原文法G中添加一个新的开始符号S'和产生式S' -> S。这是为了确保分析只有一个接受状态。
  • 闭包计算要彻底:看到点后面是非终结符,就要把它所有产生式的“点在最左端”的项目都加进来,直到加不进新的为止。
  • 区分状态和项目集:DFA的每个状态对应一个项目集。画图时,圆圈里写的是项目集编号(如I0, I1),而转移边上的符号是文法符号。
  • 考试通常只考到这里:即画出完整的LR(0)项目集规范族和DFA。后续的SLR(1)分析表构造虽然原理简单(根据FOLLOW集确定归约符号),但极其繁琐,在有限考试时间内通常不会要求完整构造,但可能会给一个简单的DFA,让你判断是否是SLR(1)文法(即是否存在移进-归约或归约-归约冲突)。

4.4 题型四:语法制导定义与中间代码生成

这类题目给出一段代码或一个语法结构,以及对应的语法制导定义(属性文法),要求你画出带注释的语法树,并展示属性计算过程,或者直接写出生成的三地址码、四元式序列。

解题要点

  1. 理解继承属性与综合属性
    • 综合属性:自底向上计算,子节点的属性值用于计算父节点的属性值。比如表达式的“值”。
    • 继承属性:自顶向下或水平传递,父节点或兄弟节点的属性值用于计算当前节点的属性值。比如变量的“类型”或“存储地址”。
  2. 画出分析树并标注属性:根据语法分析过程画出分析树,然后根据语法制导定义中的规则,像做算术题一样,从已知的(如词法值)开始,逐步计算出每个节点的属性。继承属性通常需要从左兄弟或父节点获得初始值。
  3. 生成三地址码:三地址码的基本形式是x = y op z。对于赋值、算术运算、数组访问、控制流(if,while)都有固定的翻译模式(模板)。你需要熟记这些模板:
    • while (E) S的翻译:
      L1: code for E to evaluate condition, result in t ifFalse t goto L2 code for S goto L1 L2: ...
    • if (E) S1 else S2的翻译:
      code for E to evaluate condition, result in t ifFalse t goto L1 code for S1 goto L2 L1: code for S2 L2: ...
    • 关键技巧:合理使用临时变量(t1, t2...)和标签(L1, L2...),并注意代码生成的顺序。可以边模拟语法分析(特别是LR分析)的过程,边在归约时调用相应的语义动作来生成代码。

5. 复习策略与考场应对技巧

最后,分享一些宏观的复习策略和考场上的实战技巧。

5.1 高效的复习路径规划

  1. 总览地图(1天):快速回顾教材目录和课堂笔记,画出编译六个阶段的流程图,明确每个阶段的核心任务和输出。做到心中有全局。
  2. 攻坚核心(3-4天):集中火力攻克语法分析(LL和LR)和语法制导翻译。这是分值最重、最硬核的部分。反复练习FIRST/FOLLOW集计算、预测分析表构造、LR(0)项目集DFA绘制、以及三地址码生成。每类题至少亲手做3-5道典型例题。
  3. 扫清其余(1-2天):复习词法分析(正则表达式与自动机)、语义分析(类型系统、符号表)、代码优化(常见优化技术)和目标代码生成(基本概念)。这些部分通常考得比较浅,以概念理解和简答为主。
  4. 真题模拟(1-2天):找近几年的期末考试真题,严格按照考试时间进行模拟。目的不是猜题,而是熟悉题型、分配时间和发现自己的薄弱环节。考后认真订正,针对错题回溯对应的知识点。
  5. 查漏补缺(考前1天):不再做新题,快速翻阅自己整理的错题本、核心公式(如FIRST/FOLLOW计算规则)、和重要的流程图(如编译流程、LL/LR分析算法步骤)。让大脑保持清晰的结构。

5.2 考场上的时间分配与答题要诀

  • 时间分配:通常考试时间2-3小时。拿到试卷先花2分钟快速浏览全部题目,对难度和题量有个估计。建议将时间大致分为:概念简答(15-20%)、计算与构造(60-70%)、综合设计(15-20%)。给计算题留足时间。
  • 答题顺序:从易到难,先做有把握的概念题和简单计算,建立信心,拿下基础分。然后再攻克复杂的文法改造、LR项目集等大题。最后处理可能的设计题。
  • 计算题书写规范
    • FIRST/FOLLOW集:务必写出计算过程,至少写出关键推导步骤。例如:“因为A -> Bc,且FIRST(B) = {b, ε},所以FIRST(A)包含FIRST(B)中非ε的元素{b};又因为ε ∈ FIRST(B),所以还要继续看c,加入FIRST(c)={c}。故FIRST(A) = {b, c}。” 这样即使结果错了,过程分也能拿到。
    • 预测分析表/LR分析表:画表格要清晰,行列对齐。填表时,把对应的产生式完整写上去。
    • 画图题(自动机、语法树、DFA):用尺子画,状态、符号标注清楚。图是重要的得分点,潦草可能导致误判。
  • 面对难题:如果某一大题卡住(比如LR项目集状态太多,一时混乱),不要死磕超过10分钟。果断跳过,做后面的题目。所有题目做完后再回头思考。有时做后面的题会给你带来灵感。对于完全没思路的题,尽量写出相关的定义、公式或第一步,争取部分分数。

编译原理的复习,是一个将抽象理论具象化、将零散知识系统化的过程。它考验的不是死记硬背,而是逻辑理解和系统构建能力。当你能够不看书,在白纸上从词法分析到目标代码生成把整个流程串讲下来,并对其中每个关键算法(如子集构造、FIRST集计算、LL/LR分析过程)的步骤了然于胸时,你就真正掌握了这门课的精髓,面对期末考试自然也能从容应对。这门课的知识或许在日常编程中不会直接用到,但它赋予你的那种对程序本质的深刻理解力和系统化思维,将会在你未来的技术生涯中持续发光。