哈工大编译原理复习全攻略:词法分析、语法分析与实验避坑指南
2026/9/18 17:55:47 网站建设 项目流程

1. 先说清楚:哈工大编译原理这门课,难在哪里

我在不少群里看到过一句话:哈工大的计算机课里,最让人“听过但学不懂”的就是编译原理。数据结构和操作系统好歹还能靠画图理解个大概,编译原理不一样,你刚觉得自己听懂了DFA最小化,下一讲PPT直接开始推LR(1)项目集,一节课下来草稿纸画满,脑子却是空的。

这门课难,不是因为它有多么高深的数学,而是它的知识密度太大,而且每个模块都建立在前面模块的基础上。词法分析要啃正则和自动机,语法分析要顺着上下文无关文法一路走到LR分析表,后面的语义分析、中间代码生成又依赖你对分析树和符号表的理解。换句话说,中间任何一个章节“瘸腿”,后面基本是跟着课件硬抄。

考试也是这样,覆盖范围从词法分析一路考到代码优化,看起来吓人,但仔细看历年题会发现,真正的大分值计算题集中在词法分析和语法分析这两块,而这些恰恰是最能通过反复做题练出来分数的环节。

这篇文章就是给你一条实际的复习路径:资料怎么挑怎么用、六周时间怎么排、每个考点到底在考什么、词法分析实验怎么写才能少踩坑,以及最后怎么把课程内容延伸到面试题上去。无论你是正在修这门课、准备期末,还是考研复试需要重新捡起编译原理,都可以直接照着这个思路走。

1.1 为什么这门课让不少同学栽跟头

编译原理的课堂节奏普遍偏快。老师默认你离散数学里的集合运算、关系闭包掌握得差不多,也默认你能接受“这一页PPT跳过了三步推导,你回去自己补”。但实际情况是,大部分同学第一次接触NFA转DFA、第一次看到ACTION表和GOTO表的时候,连这些概念对应的物理意义都没建立起来。

再加上编译原理不像操作系统那样有非常多形象的生活类比,中文教材写得很严谨,但严谨的代价是抽象,读完前辈推荐的陈火旺第三版之后,对“怎么分析一个句子”有了点感觉,可到了“为这个文法构造LR分析表”的题目,还是拿不下笔。这时候真正缺的不是努力,是一套“考试导向”的复习框架。

1.2 这门课的知识体系地图

把这门课拆开看,就是一条从源码到目标代码的流水线,每个阶段对应教材里的大章节。我在复习时做了一张模块表,基本决定了后面所有的时间分配。

模块核心内容典型题型权重估算
词法分析正则表达式、NFA/DFA、最小化构造NFA、子集构造、DFA最小化25%-30%
语法分析LL(1)、LR(0)/SLR/LR(1)/LALRFirst/Follow/预测分析表、项目集族35%-40%
语义分析与中间代码语法制导定义、三地址码、回填翻译布尔表达式、生成中间代码15%-20%
代码优化与目标生成基本块、DAG、寄存器分配划分基本块、优化等价变换10%左右

这张表是我复习时最重要的参考。语法分析占比接近一半,词法分析又是后面所有内容的地基,把这两块啃下来,期末基本就稳了;语义分析和优化部分反而不用死磕偏题,把基本概念和经典题型过一遍足矣。

2. 资料清单:课件、教材、视频、真题分别怎么用

对于哈工大编译原理的资料,网上能搜到一个常见组合:学校课件讲义、陈火旺《编译原理(第三版)》加配套习题解答、MOOC视频,以及学长学姐整理的选择题题库和实验报告模板。我在复习前把这些资料全凑齐了,但真正用起来之后发现,资料不是越多越好,关键在于给每类资料定好“身份”。

这就像做菜一样,课件是主菜,教材是菜谱,视频是试吃券,真题是最后的检验工具。你用错了次序,比如一开始就抱着一本编译原理第三版从第一章读到第八章,大概率一周之后就想放弃了。

2.1 学校课件讲义:复习的第一主线

课件是所有资料的锚点。哈工大的编译原理课件整理得很系统,章节顺序基本按照“词法→语法→语义→优化”展开,每章都有对应的例题和伪代码。考试命题的范围基本不会跳出课件,尤其是在概念题上面,很多选择题的答案可以直接对应到课件里的一页PPT。

课件有两个用法。

第一遍学习时,把课件当作阅读材料,标记出那些“老师没展开但课后要求掌握”的灰色地带,比如正则表达式各种运算符的优先级问题。第二遍复习时,把一个章节的课件铺在一张A4纸上,自己默写出本章结构,再对照课件找漏。这种“输出式复习”比单纯看课件管用得多。

获取渠道上,学校教学平台和课程群一般都会放官方版,公开的GitHub仓库也能搜到一些整理好的讲义集合,但有一点必须提醒:不同学期的课件会有增删,考试以本学期老师发的版本为准,别拿着往年的旧版本去背新学期的概念题。

2.2 教材与习题解答:用来查定义,不是从头啃

陈火旺的《编译原理(第3版)》是国内很多高校的指定教材,哈工大也一直在用。这本书最大的优点是定义极其严谨,适合回答“什么是活前缀”“什么是句柄”这类概念题,以及当你对课件里的某个术语理解不清时,回到教材里查原文定义。

但它有一个问题:例子偏老,有些描述不够直观,比如对LR分析过程和语法制导翻译的讲解,第一次看会觉得很绕。所以我的建议是不要把教材当作主线,而是把它当案头工具书,遇到课件上跳步的知识点,就翻到教材对应章节,把定义读一遍,然后立刻回到题目里验证。

至于配套的习题解答(第三版答案),价值在于课后题。编译原理的课后题质量很高,尤其是词法分析和语法分析部分的计算题,很多期末题的概念和思路就是从中改编而来的。但不要一道一道全做,我建议只做每章的奇数题和带星号的题,时间不够时可以直接对着答案逆向理解解题步骤。

2.3 视频课程与公开资源:用倍速捡漏

MOOC上有哈工大团队的编译原理课程,内容跟线下课基本对得上。第一遍听课没跟上进度的同学,可以针对不懂的章节去单独看对应的视频,比如只看“自底向上分析”那两三讲。

看视频有一个技巧:不要从第一节看到最后一节,那是重复学习,效率很低。你要把视频当成一个“资源包”,每次只取自己卡壳的那10分钟。比如你在构造预测分析表时总是漏掉Foll ow集里的#号,那就单独回看Follow集计算的片段,看完立刻回去做题验证。

轻易不相信的话,可以去GitHub搜“编译原理笔记”,很多学校的学长学姐都整理了非常详细的Markdown笔记,内容比PPT更口语化,适合快速建立全局印象。另外,网上还能搜到吉林大学等高校的课件和题库,这些可以拿来做额外练习,但注意优先吃透本校内容,别把精力分散到其他学校的偏题怪题上。

2.4 历年真题和仿真题:最后两周使用

很多同学到处找哈工大编译原理的期末真题,说实话,能把历年的完整卷子凑齐的人非常少,流传出来的更多是回忆版和题目片段。但这不影响复习效果,因为编译原理的题型高度固定。

你可以根据回忆版题目整理出三类必考内容:第一类是概念选择题,覆盖每个模块的基础定义;第二类是计算推导题,比如给一个简单文法要求求First集和Follow集、构造预测分析表,或者给一个文法要求构造LR(0)项目集族;第三类是综合分析题,通常是把一个程序片段翻译成三地址码,再划出基本块做简单优化。

我在复习时把这些题型整理成了一个目录,然后把手头能找到的所有模拟题、题库、教材课后题按题型归档。这样做的效果很好,因为到后期,我一看到题目,条件反射就知道它在考察哪个考点、该用哪一套步骤去解。

3. 复习节奏:从第十六周倒推,六周怎么安排

哈工大编译原理一般到第17周左右就会举行期末考试,课程大约16周结束。这里我以“考前6周”为一个完整的复习周期来规划,如果你时间更紧,可以把前两周压缩成一周,但框架不变。

这个节奏最重要的原则是:前两周必须把词法分析彻底搞定,因为它是后面所有内容的地基;中间两周主攻语法分析,这是分值的大头;最后两周处理语义分析和优化,并且开始成套刷题。如果你按“从头到尾顺序看一遍”的方式复习,到了第五周可能还在语法分析部分打转,后面就全完了。

3.1 总体思路:先保证计算题,再啃概念题

我观察到,期末复习时最容易出现的错误,是花大量时间背概念,导致计算题练习量不足。但实际上,概念题背一背就能拿分,计算题不亲手练是绝对拿不到的。

语法分析里的预测分析表和LR项目集构造,看起来有规律可循,但真正动手时会因为一个小符号的遗漏而全盘出错。这种操作技能没有捷径,只能依靠反复手算形成肌肉记忆。所以我的总体安排是:每天固定两小时,一小时做题,一小时总结错题,不要听课件、看视频超过半小时。

3.2 每周安排与参考用时

下面是我复习时实际执行的周计划表,你可以根据自己的起始水平调整。

周次阶段目标核心任务验收标准
第1周打牢词法分析正则表达式练习、NFA构造、NFA转DFA、DFA最小化能独立完成一个中等复杂正则的完整转换链
第2周进入语法分析上下文无关文法、推导与归约、句柄与素短语能指出一个句型的句柄,并理解分析树与文法的关系
第3周搞定LL(1)计算First集和Follow集、判断LL(1)、构造预测分析表给任意简单文法,能在20分钟内完成预测表
第4周拿下LR族LR(0)项目集、SLR(1)分析表、理解LR(1)与LALR能手工构造一个小文法的SLR分析表
第5周语义分析与优化语法制导翻译、三地址码、基本块划分能把布尔表达式和控制流语句翻译为三地址码
第6周冲刺与成套练习过一遍全部选择考点,做3套仿真题,整理错题错题率控制在20%以内

第一周和第二周如果基础很弱,可以适当延长,但绝不能无限期拖延。我自己的经验是,第三周必须强制进入语法分析,因为LL(1)和LR这两块内容在考前的最后几天如果还没有练熟,做题时容易在细节处反复出错,心态特别容易崩。

关于“验收标准”,我的建议是每完成一个阶段,找一张白纸,在没有任何参考资料的情况下,完整做一道对应的典型计算题。能写对就是真的过关了,不能写对就必须回头重练,没有任何商量余地。

4. 考点逐个拆:词法分析、语法分析、语义分析与优化

这一部分才是全文的干货核心。我不按教材目录给你罗列知识点,而是把每个模块里最容易考、也最容易出错的点,用实战的角度拆开讲清楚。

4.1 词法分析:正则、NFA、DFA三件套

词法分析部分,期末必考的核心技能是一整条转换链:正则表达式 → NFA → DFA → 最小化DFA。这条链的三个步骤,你每一步都需要亲手练熟。

第一步,把正则表达式转换为NFA,常用的是Thompson构造法。考试通常不会给你特别复杂的正则,常见的包括a(a|b)*b(a|b)*abbletter(letter|digit)*这一类。Thompson构造法的核心是规则化:每个符号对应一个小的子图,每个运算符(连接、并、闭包)都有固定的拼接模式。你只需要记住这三张基本图,然后像搭积木一样组合起来就行。

第二步,NFA转DFA,即子集构造法。这里有一个不少人都犯过的错误——只关注状态集合的字母表变化,却忘了在最终DFA里标记每个状态是否是终态。终态判断依据是:该状态集合里是否包含原NFA任一终态。一旦漏标记,后面DFA最小化直接错。

第三步,DFA最小化,很多人觉得难,其实它的基础思想是划分等价类。先把终态和非终态分成两个集合,然后反复对每个集合进行细分,直到每个集合内状态无法再分化。这里的核心操作是:对每个状态,看在某个输入符号下它跳转到哪个集合,如果同一个集合内两个状态跳转去处不在一类,就需要分裂。

我在复习时发现一个特别有效的验证方法:把DFA最小化完成后,自己随便挑一个由该DFA接受的字符串和一个拒绝的字符串,从头完整走一遍,检查结果是否符合你对正则语言的预期。这个小测试能抓出90%的细节错误。

4.2 语法分析:LL(1)的First/Follow与预测表

进入语法分析,第一步是算First集和Follow集。解题公式其实就几条,但很多同学在计算时经常漏项。First集要看产生式右部首符号:如果首符号是终结符,直接加入;如果是非终结符,就把它的First集加进来;如果能推出空串,继续往后看下一个符号。Follow集则是看产生式右部中该非终结符后面跟什么,这里最容易漏的两类是开始符号的Follow集里要有#,以及右部末尾非终结符的Follow集要继承左部的Follow集。

LL(1)的判断条件是一个关键考点:同一个非终结符的多个产生式,它们的First集两两不相交,且如果某个产生式能推出空串,该非终结符的Follow集不能包含冲突。判断本身不难,难在很多人算完First和Follow后不愿意逐条检查冲突,导致预测分析表构造错误。

预测分析表构造就是嵌套循环:对每个产生式A → α,把A所在的行和First(α)里的每个终结符所在的列交叉点填上这个产生式,如果α能推出空串,还要对Follow(A)里的每个终结符在A行对应列填上产生式。填表过程实际上是对First和Follow理解的实战检验,所以我在复习时建议先把这两个集合的计算练到不假思索,再开工填预测表。

4.3 语法分析:LR族分析表的构造逻辑

LR系列是这门课最大的坎。但说实话,考试对LR的要求通常是到SLR(1),只要把LR(0)项目集族构造熟练,SLR(1)就是临门一脚。LR(1)和LALR多数时候只会考概念和简要构造,不会要求你对一个复杂的文法完整画出全部项目集。

LR(0)项目集族的构造分两步。第一步是增广文法,加入S' → S。第二步是从初始项目集开始,不断做闭包和转移。闭包操作是:如果一个项目集里某个项目形如A → α·Bβ,那么把B的所有产生式项B → ·γ都加入项目集。转移是:当点号后面是一个符号X时,将所有点号在X前的项目向后移动一位,形成新项目集。

构造完项目集族之后,关键一步是识别冲突。如果某个项目集里同时存在移进项目和归约项目,或者两个归约项目冲突,就说明这个文法是LR(0)文法不成,但可能通过SLR(1)方法解决——归约条件加上Follow集作为向前看符号,就能过滤掉部分冲突。这块如果要简化成一句话,那就是:LR(0)看项目集是否冲突,SLR(1)再看冲突能否用Follow集区分。

手工构造一张完整的SLR分析表,考试里通常会给一个小文法,10到12个状态左右。我的建议是第一遍在草稿纸上画,第二遍在整理笔记时重新画一遍,你会发现第二遍的速度和准确性都大幅提升。这种“双遍复写”的方法对我记忆项目集和ACTION表帮助巨大。

4.4 语义分析与中间代码生成、代码优化考点

语义分析部分,最常考的是语法制导翻译。你需要清楚两类概念:综合属性和继承属性。综合属性自下而上计算,继承属性自上而下传递,这直接决定了在分析树里如何标注属性值。

中间代码生成的核心是三地址码。考试里常见的题型有两种:一是把算术表达式转换成三地址码序列;二是把布尔表达式或if-else语句翻译成带跳转语句的三地址码。布尔表达式的翻译常用回填技术,这里最容易错的是“真出口”和“假出口”的编号,做题时要不厌其烦地在草稿纸上一条条标出跳转目标,避免凭直觉填编号。

代码优化部分,考试以基本块划分、DAG构造、公共子表达式删除、死代码删除、代码外提和强度削减这些经典方法为主。基本块划分的判断很简单:遇到入口语句就划分一个新的基本块。DAG构造稍微复杂一点,但本质上就是记录变量的赋值链条,你按部就班地给每个节点编号、标注变量名,最后再根据DAG重新生成代码,优化效果一目了然。

这部分的策略是理解核心思想,不要贪多求偏。概念选择题里出现的优化方法名词,只要你能在试卷上写出一句话解释,基本就能拿分,比如“代码外提是把循环中不变的计算移到循环外”。

5. 词法分析实验:框架、代码与踩坑记录

实验是编译原理课绕不开的一部分,也是很多人的焦虑来源。词法分析实验的要求一般很明确:读入一个源程序文件,输出它的token串,识别出关键字、标识符、无符号数、运算符和分隔符,同时过滤掉注释和空白符。

很多人拿到题目先想“我要不要直接用正则库split一下完事”,但课程要求通常会限定你必须体现“自动机的思想”,也就是说你要自己写出一个类似DFA的扫描逻辑。从学习角度,我更推荐你用状态转移的方式实现,因为这才是这门课要训练的核心能力。

5.1 实验要求与整体框架

一个典型的词法分析实验可以拆成几个模块:输入缓冲、状态转移核心、关键字表、输出结果。

输入缓冲建议使用逐字符读取的方式。我在写的时候用了两个指针,一个指向当前字符,一个负责向前看一个字符,这样处理>=<===这种复合运算符时就很方便。状态转移核心是每个识别阶段对应一个状态编号,比如状态0是初始状态,状态1是在识别标识符过程中,状态2是在识别数字过程中,状态3是在处理注释中等。

整体框架可以这样设计:

public class Lexer { private String input; private int pos; private List<Token> tokens = new ArrayList<>(); public List<Token> analyze() { while (pos < input.length()) { char c = peek(); if (Character.isLetter(c) || c == '_') { readIdentifierOrKeyword(); } else if (Character.isDigit(c)) { readNumber(); } else if (isOperator(c) || isSeparator(c)) { readOperatorOrSeparator(); } else if (c == '/' && peekNext() == '*') { skipComment(); } else { pos++; } } return tokens; } }

上面的代码只是一个骨架,但你能看到,整个词法分析器的核心是“根据当前字符决定进入哪个识别子程序”,这正是DFA思想的体现。识别出单词后,统一交给一个方法去判断它是关键字还是普通标识符——判断方式建议先查保留字表,命中就是关键字,否则就是标识符。

5.2 状态机实现的思考

如果你想让实验报告更有深度,可以专门用一个枚举来定义状态,然后写一个主循环:根据当前状态和输入字符查状态转移表,决定跳转到下一个状态或输出一个token。这种写法更符合编译原理教材里“状态转换图”的描述,老师一看就觉得你是真正理解了的。

我当初实现时用了比较朴素的方式:

  • 初始状态遇到字母或下划线,进入标识符状态,不断读字母数字下划线直到遇到非这些字符,然后查关键字表;
  • 初始状态遇到数字,进入数字状态,支持整数和小数,也可以顺带支持科学计数法(遇到了指数e/E);
  • 初始状态遇到/,向前看是不是*,是则进入注释状态,一直读到*/为止;
  • 其他运算符和分隔符,直接按单个或多字符匹配输出。

其实不需要把所有状态枚举得过于精细,只要把“状态转移”的意图体现出来,并且代码清晰、注释完整,实验报告就能拿到不错的分数。

5.3 实验里最容易出问题的几个细节

第一个坑是关键字和标识符的冲突。你必须在识别完一个完整单词后,再用一张HashSet去判断它是不是关键字,不能在读到第一个字母时就判断。比如intx应该是一个普通标识符,绝对不能被识别成关键字int加标识符x

第二个坑是注释的边界条件。很多C语言实验在读取/*之后,会傻傻地找*/,但如果文件末尾没有闭合注释,程序就会越界。写代码时一定要判断pos是否已经超过字符串长度,提前退出并报错。

第三个坑是行号和列号的统计。如果实验要求输出token所在行列,你必须在每次遇到换行符时更新行号,同时把列号归零。这个逻辑本身不难,但容易忘,而且一旦忘了,调试起来非常闹心。

第四个坑是输出格式。交实验报告前一定要看清要求:是输出(类别码, 属性值)二元组,还是(所在行, 类别码, 属性值)三元组。不同老师要求不一样,格式错了会扣不少分。我在交之前写了一个小脚本,把几个典型测试用例全部跑一遍,确认输出格式完全一致后才提交。

6. 期末题型与面试备考的延伸建议

拿到期末复习的资料和实验分之后,最后一步是把课程知识转化成卷面分数,以及为后续的面试场景做好铺垫。很多同学考完编译原理就彻底把它丢了,等到找实习时被问到一个“编译器前端和后端的区别”又瞬间懵掉,这其实是挺可惜的。

6.1 选择题、计算题、综合题的准备策略

期末试卷的题型分布一般在三种类型之间:概念选择题、计算推导题、综合分析题。

选择题考察的知识点比较散,但重复率很高。我把高频考点整理成了一个清单:编译程序与解释程序的区别、编译过程的五个阶段、前端和后端的划分、正规文法与正规式的关系、DFA和NFA的等价性、自顶向下和自底向上的代表分析方法、语法制导翻译中综合属性和继承属性的区分、中间代码的常见形式、各种优化方法分别属于哪类优化。这些内容建议用表格整理成一页A4纸,考前反复默念几遍就足够。

计算推导题就是我在第4章写的那几类:NFA/DFA链、First/Follow/预测表、LR项目集/分析表、三地址码生成、基本块/DAG/优化。这部分不存在捷径,只有反复练习。我在冲刺阶段要求自己每天至少动手完整算两道计算题,直到草稿纸上的步骤和答案完全一致为止。

综合分析题通常是把一个循环或者条件语句的源代码给你,要求完成“翻译成中间代码 → 划分基本块 → 做优化”的一条龙操作。这类题的难点是步骤之间环环相扣,中间任何一步出错,后面全部白做。我的建议是做之前先在题目旁边画一条横线,把“代码”和“三地址码”分开,每一步都标上编号,检查时按编号回溯,能省下大量时间。

6.2 从期末考试延伸到面试题

如果时间允许,我建议你在课程结束后把编译原理的核心内容整理成几个“面试向”的回答框架。这不是要你做额外的项目,只是把课内知识换一种表达方式,在技术面试里你会重新发现这门课的价值。

比如“编译器前端和后端如何划分”这个经典问题,前端负责与源语言相关的部分——词法分析、语法分析、语义分析和中间代码生成,后端负责与目标机器相关的部分——代码优化、寄存器分配、目标代码生成。回答时能顺带提一句“这种划分让编译器更容易移植,更换目标平台只需要重写后端”,就能体现你对系统设计的理解。

再比如“LL(1)和LR的区别”这个问题,可以从分析方式上回答:LL是自顶向下的、利用当前输入符号做预测;LR是自底向上的、利用历史符号和向前看符号做规约。能进一步指出LR比LL识别的文法范围更广、但分析器构造更复杂,就足够让面试官觉得你是真学过而不是背的。

还有一个高频问题:“为什么要有中间代码?”核心答案是便于代码优化和跨平台移植。你在期末复习时如果认真翻译过三地址码,这个问题的答案会脱口而出。

我把这些常见面试题整理成了一个自查表,你可以对着它做自问自答:

问题核心回答要点
词法分析和语法分析的区别词法识别单词,语法识别句子结构
正则表达式为什么适合词法分析有限自动机的表达能力刚好覆盖单词形态
自顶向下和自底向上分析的本质区别推导方向不同:从开始符号推句子还是从句子规约回开始符号
LR分析表的两个核心动作ACTION表决定移进/规约/接受,GOTO表决定状态转移
静态类型检查与动态类型检查类型错误发生的时机不同,一个编译期一个运行期
中间代码有哪些常见形式三地址码、四元式、逆波兰式、DAG等
代码优化为什么不能无限做优化可能改变语义、增加编译开销、数据流分析复杂

这份自查表也完全可以作为期末复习的总结提纲。如果你能在期末考试前,把上表每一个问题都用一段完整通顺的中文解释出来,那你这学期编译原理掌握得基本达标了。

最后分享一个我自己的小习惯:复习最后一周,我把每个模块的“解题操作链”写在一张A4纸上,等考试那天进考场前只看这张纸。词法分析那一栏就写“正则→Thompson→子集构造→最小化”,LR那一栏写“增广文法→项目集→ACTION/GOTO表→冲突检查”,这样在考场上碰到任何一道大题,我都能迅速定位到自己处在这个操作链的哪个环节,稳得不慌。希望你也能有一张属于自己的A4纸。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询