资讯详情

零基础入门编译原理:从词法分析到语法分析,手写第一个解析器

📅 2026/10/19 5:56:14 | 华诺云谱 👁 阅读
零基础入门编译原理:从词法分析到语法分析,手写第一个解析器
简介这份《编译原理学习指导》文档面向计算机专业本科生、考研备考者以及希望深入理解编译器底层机制的开发者系统梳理了编译原理的核心知识框架与学习路径。内容围绕词法分析、语法分析LL算法、递归下降算法、LR算法、语义分析、运行时环境、中间代码生成与代码优化等关键阶段展开并重点推荐了《Compilers: Principles, Techniques, and Tools》龙书、《Modern Compiler Design》和《编译原理及实践》三本经典教材对比了它们在理论深度与实践侧重上的差异帮助读者根据自身需求选择合适的学习材料。资源包为1个doc文档大小约34KB结构紧凑适合作为课程复习提纲或考研知识点速查手册。目前已有153人学习浏览。通过这份指导读者可以快速建立编译流程的整体认知理解算法本质比文字描述更为关键并借助Tiny C等实践案例逐步掌握编译器构建的基本方法为后续深入学习或自行设计小型编译器打下基础。1. 编译原理入门从“这课到底在讲啥”到敢动手写第一行很多人第一次翻开编译原理的教材翻不了几页就合上了。满屏的正则表达式、自动机、LL(1) 分析表、LR 项目集规范族看着像天书。你可能会想我又不打算去写一个 TurboC 或者 Java 编译器学这门课到底图什么其实换个角度就通了——你写的每一行代码从int a 1;到屏幕上跑出结果中间必须经过一个“翻译官”。这个翻译官就是编译器。编译原理讲的就是这个翻译官怎么把人类能看懂的高级语言一步步变成机器能执行的指令。它不教你“怎么用某个工具”而是教你“一个程序如何理解另一个程序”。这件事想清楚了后面那些看起来吓人的理论其实都是在回答同一个问题怎么让机器准确、高效地完成翻译。这门课适合所有想真正搞懂“代码到底怎么跑起来”的人零基础也能上手前提是别一上来就死磕 LR 算法。2. 为什么值得学编译原理到底能给你带来什么2.1 它训练的是“把复杂问题拆成阶段”的思维编译原理最值钱的地方不是让你去造一个编译器而是让你学会一种工程思维把一个巨大的、看起来无从下手的问题拆成若干个可以独立解决的阶段。编译器把“翻译”这件事拆成了词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成。每个阶段只干一件事输出交给下一个阶段。这种“流水线式”的拆解思路你在做任何稍大的软件项目时都用得上。比如你要写一个配置文件解析器、一个查询语句处理器、一个模板引擎本质上都是缩小版的编译过程。学完词法分析和语法分析你会发现以前觉得神秘的“程序怎么读懂输入”这件事突然有了清晰的套路。2.2 面试和考试里它是区分“会用”和“懂原理”的分水岭很多计算机专业的考研把编译原理列为必考不是故意为难人。因为这门课能同时考察几样硬功夫形式语言的基本概念、算法设计能力、数据结构尤其是树和图的运用、以及把理论落到代码的工程能力。面试里问“一个表达式怎么求值”如果你能说出“先词法切成 token再语法分析建 AST然后遍历 AST 求值”和只说“用栈模拟一下”给人的感觉完全不一样。前者说明你脑子里有完整的处理链路后者只是背了一个解法。这种链路思维在排查线上问题时特别管用——你知道一个字符串从输入到执行中间经过了哪些环节就能快速定位是哪一层出了毛病。2.3 零基础入门先抓住“输入→输出”这条主线新手最容易犯的错是一头扎进自动机理论里推了半天状态转移却不知道这些东西在编译器里到底对应哪一步。入门阶段建议先建立一条主线源代码字符串 → 词法分析器 → token 序列 → 语法分析器 → 抽象语法树 → 语义检查 → 中间代码 → 目标代码。你不需要一开始就搞懂每个环节的算法细节但要清楚每个环节的输入是什么、输出是什么、为什么要存在。比如词法分析的输入是一串字符输出是一个个有类型的 token语法分析的输入是 token 序列输出是一棵树。把这条主线刻在脑子里再去学具体算法你就知道每个算法是在解决哪个环节的问题不会迷路。3. 零基础怎么迈第一步从词法分析开始动手3.1 先搞懂一个概念词法分析到底在做什么词法分析的任务简单说就是“把长字符串切成一个个有意义的单词”。比如你写int age 25;词法分析器看到的是int、age、、25、;这五个 token。每个 token 有自己的类型关键字、标识符、运算符、数字字面量、分隔符。为什么要有这一步因为后面的语法分析不想直接面对一堆乱七八糟的字符它希望输入是规整的、带类型标记的单元。这就像你读英文句子先按空格和标点把句子拆成单词再去分析语法结构。词法分析就是干这个拆单词的活。入门阶段你不需要一上来就写正则表达式引擎先用最朴素的方式把 token 切出来跑通再说。3.2 第一个能跑的例子手写一个极简词法分析器下面这段 Python 代码实现了一个最简词法分析器能识别整数、标识符、加号和分号。你把它复制到一个.py文件里就能直接运行。# 极简词法分析器把输入字符串切成 token 列表 def tokenize(source): tokens [] # 存放结果的列表 i 0 # 当前扫描位置 while i len(source): ch source[i] # 跳过空白字符 if ch.isspace(): i 1 continue # 识别整数连续的数字字符 if ch.isdigit(): num while i len(source) and source[i].isdigit(): num source[i] i 1 tokens.append((NUMBER, num)) continue # 识别标识符字母开头后跟字母或数字 if ch.isalpha(): name while i len(source) and (source[i].isalnum()): name source[i] i 1 tokens.append((IDENT, name)) continue # 识别单字符运算符和分隔符 if ch in ;: tokens.append((SYMBOL, ch)) i 1 continue # 遇到不认识的字符直接报错 raise ValueError(f无法识别的字符: {ch}) return tokens # 测试一下 result tokenize(age 25 count;) for t in result: print(t)运行后你会看到类似这样的输出(IDENT, age) (SYMBOL, ) (NUMBER, 25) (SYMBOL, ) (IDENT, count) (SYMBOL, ;)这段代码的逻辑很直白从左到右扫描字符串遇到空白就跳过遇到数字就连续读取组成一个整数 token遇到字母就连续读取组成标识符 token遇到、、;就单独作为一个符号 token。最后返回一个列表每个元素是(类型, 值)的二元组。这就是词法分析器最核心的工作方式。你可能会觉得“这也太简单了”没错入门阶段就是要先跑通这个最简单的版本建立信心。后面你学的正则表达式和有限自动机本质上是在用更系统、更强大的方式来做同一件事。3.3 从手写代码到理解“正则表达式→自动机”的路径上面那个手写版本能跑但它有个问题如果语言里有一百种 token 类型你就要写一百个if分支代码会变得又长又难维护。编译原理给出的解决方案是用正则表达式描述每种 token 的模式然后自动把正则表达式转换成有限自动机再由自动机生成词法分析器的代码。这条路径是正则表达式 → NFA非确定有限自动机→ DFA确定有限自动机→ 代码。入门阶段你不需要手推 NFA 到 DFA 的子集构造算法但要知道这条路径的存在。很多工具比如 Lex 或 flex就是帮你自动完成这条路径的。你只需要写好正则表达式规则工具帮你生成词法分析器的 C 代码。所以学词法分析的正确姿势是先手写一个能跑的简单版本理解“扫描字符、识别模式、输出 token”这个核心过程然后再去理解正则表达式和自动机是怎么把这个过程系统化的。4. 语法分析入门把 token 序列变成一棵树4.1 为什么需要语法分析token 序列还不够“结构化”词法分析输出的 token 序列是一维的、扁平的。但程序的结构是嵌套的、有层次的。比如1 2 * 3你不能简单地从左到右算因为乘法优先级高于加法。语法分析的任务就是根据语言的语法规则把扁平的 token 序列组织成一棵抽象语法树AST。在这棵树里*的节点在的节点下面优先级关系一目了然。有了这棵树后面的语义分析和代码生成就可以递归地遍历它机械地完成工作。所以语法分析的核心目标是输入 token 序列输出一棵能体现程序结构的树。4.2 递归下降算法最适合新手入门的语法分析写法递归下降是一种自顶向下的语法分析方法思路非常符合直觉为语法里的每个非终结符写一个函数函数内部根据当前 token 决定走哪条产生式。下面是一个能解析加减法表达式的递归下降分析器它直接生成 AST。# 递归下降语法分析器解析加减法表达式生成 AST class Token: def __init__(self, type_, value): self.type type_ # token 类型 self.value value # token 值 class Parser: def __init__(self, tokens): self.tokens tokens # token 列表 self.pos 0 # 当前读取位置 def peek(self): # 查看当前 token不移动位置 if self.pos len(self.tokens): return self.tokens[self.pos] return None def next(self): # 取出当前 token 并后移一位 tok self.tokens[self.pos] self.pos 1 return tok def parse_expr(self): # expr - term (( | -) term)* node self.parse_term() while self.peek() and self.peek().value in (, -): op self.next().value right self.parse_term() node (op, node, right) # 用元组表示树节点 return node def parse_term(self): # term - NUMBER | IDENT tok self.next() if tok.type in (NUMBER, IDENT): return tok.value raise ValueError(f语法错误: 意外的 token {tok.value}) # 测试解析 1 2 3 tokens [ Token(NUMBER, 1), Token(SYMBOL, ), Token(NUMBER, 2), Token(SYMBOL, ), Token(NUMBER, 3), ] parser Parser(tokens) ast parser.parse_expr() print(ast)运行结果是(, (, 1, 2), 3)这棵树表示先算1 2再算结果加3。parse_expr函数对应语法规则expr - term (( | -) term)*它先解析一个 term然后循环检查后面有没有加号或减号有的话就再解析一个 term并把它们组合成一个新的树节点。parse_term函数负责解析最基本的数字或标识符。这种写法之所以叫“递归下降”是因为parse_expr内部会调用parse_term而如果语法更复杂parse_term还可能反过来调用parse_expr形成递归。它的优点是代码结构直接对应语法规则可读性极强调试也方便。缺点是遇到左递归文法需要改写而且效率不如表驱动的分析器。但对于入门来说递归下降是最好的起点因为它让你亲手体会到“语法规则如何变成代码”。4.3 LL 和 LR 的入门理解先知道它们解决什么问题递归下降属于 LL 类算法从左到右扫描最左推导。LL 算法的问题是当语法有左递归时比如expr - expr term递归下降会无限循环。解决办法是改写文法消除左递归。另一大类算法是 LR从左到右扫描最右推导的逆过程它更强大能处理更多文法但手动构造分析表非常繁琐所以实践中通常用工具如 Yacc/Bison自动生成。入门阶段你不需要手推 LR 分析表但要理解LL 是自顶向下从起始符号出发试图推导出输入串LR 是自底向上从输入串出发试图归约到起始符号。两者都能完成语法分析只是策略不同。新手先把递归下降写熟知道 LL 和 LR 是两条不同的技术路线遇到 LR 的复杂理论时不要慌知道它主要是给工具用的就行。5. 避坑指南新手学编译原理最容易踩的五个坑5.1 坑一一上来就死磕 LR 算法导致放弃现象学到语法分析后半段看到 LR(0)、SLR、LALR、LR(1) 一堆名词和项目集规范族的构造过程完全看不懂觉得自己不适合学这个。原因LR 算法本身是编译器自动生成工具的内部原理手工推导极其繁琐。很多教材把它放在很重的位置导致新手误以为“学不会 LR 就等于没学编译原理”。解决入门阶段先跳过 LR 的手工构造重点掌握递归下降和 LL(1) 的基本思想。知道 LR 是另一类算法、实践中用 Yacc/Bison 生成就够了。等你把词法分析和递归下降写熟了再回头看 LR会发现它只是在解决“如何自动构造分析表”这个问题而不是什么全新的东西。5.2 坑二只看书不写代码以为看懂了就是会了现象书上的自动机图能看懂正则表达式到 NFA 的转换步骤也能跟上但一让写代码就卡住。原因编译原理的理论和实现之间有一道坎。看懂状态转移图不等于能写出扫描器理解语法规则不等于能写出递归下降函数。解决每学一个概念就写一个最小可运行的例子。学词法分析就手写一个 tokenizer学语法分析就手写一个表达式解析器。代码不用长几十行能跑通就行。写的过程中你会遇到各种书上没讲的小问题比如怎么处理空白、怎么报错这些才是真正让你学会的东西。5.3 坑三试图从零手写完整编译器结果半途而废现象看完教材后热血沸腾决定自己写一个 C 编译器写了词法分析就卡在语法分析语法分析写完发现语义分析更复杂最后不了了之。原因完整编译器的复杂度远超新手想象。光是 C 语言的语法就够写几百条产生式再加上类型系统、作用域管理、代码生成不是一个人短时间能搞定的。解决把目标缩小。先写一个能解析四则运算表达式的计算器再写一个能解析变量赋值和打印语句的迷你语言逐步增加功能。每完成一个阶段就停下来测试确保能跑通再往下走。不要一上来就挑战完整语言。5.4 坑四忽略运行时环境只关注前端现象词法分析和语法分析学得不错但一提到运行时环境、栈帧、参数传递就跳过觉得这些和“编译”没关系。原因很多教材把运行时环境放在后面加上这部分内容偏系统底层新手容易觉得枯燥。解决运行时环境恰恰是连接编译器和操作系统的桥梁。你写的代码最终怎么在机器上跑起来变量存在哪里函数调用怎么传递参数这些问题的答案都在运行时环境里。入门阶段可以先了解栈式存储分配的基本概念知道局部变量在栈上、动态内存在堆上函数调用会创建栈帧。这些知识对你理解程序执行过程非常有帮助而且比 LR 算法实用得多。5.5 坑五纠结于“要不要自己写词法/语法文件”现象想动手做编译器但卡在“词法文件和语法文件怎么写”上花大量时间手写正则和文法进展缓慢。原因写一个能覆盖完整语言的词法和语法文件本身就是一项繁重的工作。新手容易低估这个工作量。解决入门阶段直接用简化的语法子集或者参考现成的简单语言文法。很多教材会给出 Tiny C 或类似迷你语言的完整词法/语法文件拿来用就行。你的目标是理解编译流程不是重新发明一门语言的语法。等流程跑通了再逐步替换成自己定义的语法。6. 下一步学什么从能跑的例子到完整的编译流程6.1 把词法、语法、语义串起来做一个小项目你现在已经手写过一个词法分析器和一个递归下降的表达式解析器。下一步建议把它们串起来做一个能处理变量赋值和四则运算的迷你解释器。具体路径是先扩展词法分析器让它能识别更多运算符*、/、(、)和关键字比如let然后扩展语法分析器支持乘除法的优先级和括号接着加一个简单的符号表来存储变量值最后写一个求值函数递归遍历 AST 算出结果。这个项目不大但覆盖了编译前端的主要环节。做完之后你对“源代码怎么变成可执行结果”会有一个完整的体感。6.2 了解工具链Lex/Yacc 或 flex/bison 能帮你省掉大量手工活当你手写过词法和语法分析器之后再去了解 Lex 和 Yacc或者在 Windows 下用 flex 和 bison会轻松很多。这些工具的工作方式是你写规则文件它们生成 C 代码。Lex 文件里写正则表达式和对应的动作Yacc 文件里写文法产生式和对应的语义动作。工具会自动处理 NFA 到 DFA 的转换、分析表的构造这些繁琐工作。你不需要在入门阶段精通它们但要知道它们的存在知道它们能帮你把精力集中在“定义语言”而不是“实现分析算法”上。很多实际项目里的配置解析、查询解析都是用这类工具生成的。6.3 推荐的学习节奏和资源选择如果你是完全零基础建议按这个节奏走第一周手写词法分析器理解 token 的概念第二周手写递归下降的表达式解析器理解 AST第三周把两者串起来做迷你解释器第四周了解正则表达式到自动机的转换原理知道 Lex/Yacc 怎么用。教材方面入门阶段优先选那些从实际例子出发、代码量适中的书。有些经典教材理论深厚但例子偏少新手容易迷失在定义里有些偏实践的教材会带着你一步步写一个小型编译器更适合零基础跟着做。选一本你能跟着写代码的比选一本“权威但读不下去”的更重要。6.4 一个我自己的习惯每学一个算法就写一个最小可运行版本我刚开始学编译原理的时候也曾经对着 LR 分析表发呆觉得这些东西离能跑的代码太远了。后来我给自己定了一个规矩每学一个算法不管多简单都要写一个能跑的最小版本。学正则表达式匹配就写一个只支持*和|的匹配器学递归下降就写一个只支持加法的解析器学栈式代码生成就写一个把 AST 转成逆波兰输出的程序。这些代码都很短但每次跑通都会让我对“这个算法到底在干什么”多一层理解。从那以后我每次学新东西都强制自己先写一个能跑的最小例子哪怕只有十几行。这个习惯帮我省了很多“以为自己懂了其实没懂”的时间。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑