编译原理实验代码高效学习:先跑通再读懂

发布时间:2026/10/12 16:18:01
编译原理实验代码高效学习:先跑通再读懂 简介面向电子科技大学编译原理课程的学习者、编译器入门开发者及备战实验的人群实验代码完整实现词法分析与语法分析两大核心模块词法分析器识别关键字、标识符、常量与运算符生成token流语法分析器基于LL/LR解析策略构建抽象语法树。资源附带运行说明文档及可执行程序帮助快速还原实验环境并对照源码理解处理逻辑同时还包括输入处理、语义分析、代码生成等可选模块体现完整工程化思路。包内共21个文件以cpp源文件、h头文件、pas源文件为主配合docx说明文档支撑原理讲解另有工程配置、运行记录及可执行程序整体约203KB结构清晰便于按模块研读。已有2111人学习适合借鉴满分实验实现深入理解编译前端关键技术与自动机构造方法。1. 电子科技大学编译原理实验代码先跑通再读懂比先读代码高效得多说实话编译原理这门课能不能学透跟老师讲得好不好关系不大跟你手上有没有一套能跑通的实验代码关系很大。电子科技大学编译原理实验代码是很多学校同学都在找的那份完整课程设计参照它把词法分析、语法分析、语义分析、中间代码生成这几块硬骨头全部落到了一套可以用 make 一键编译、对着测试用例逐条改的 C 语言工程里。适合谁适合正在被实验报告逼疯的本科生也适合想把手写编译器当作毕设起点的研究生。很多学校用的就是王生原那本《编译原理第3版》课本讲了半学期最后能不能过关看的就是这份代码你读没读懂、改没改对。我见过太多人拿到这类代码先从头读结果读了两天还在看 lexer 的头文件反过来先跑通框架再倒推每一阶段为什么这么写一天就能把整条链路盘明白。2. 先把实验代码的骨架看懂目录结构、文法定义和实现语言怎么选2.1 一套课程实验代码的目录结构从 main.c 到 testcase我拿到任何一份编译原理实验代码第一件事永远是看目录而不是打开 src 里的源文件。课程实验代码和业务代码不一样它的模块边界就是“一趟一趟的编译过程”目录结构直接反映作者对编译流程的理解。典型的结构长这样compiler-lab/ ├── include/ │ ├── lexer.h │ ├── parser.h │ ├── ast.h │ ├── symbol.h │ └── codegen.h ├── src/ │ ├── lexer.c │ ├── parser.c │ ├── ast.c │ ├── symbol.c │ ├── codegen.c │ └── main.c ├── testcase/ │ ├── fib.c │ ├── fact.c │ ├── scope.c │ └── error.c └── Makefile这个结构几乎就是编译过程的物理映射。词法分析器产出 token 流语法分析器基于 token 流建立语法树语义分析遍历语法树做类型检查中间代码生成再遍历一遍语法树输出三地址码。每一层的输出是下一层的输入所以头文件之间的依赖关系特别重要。include 目录放公共接口src 目录放实现testcase 是可运行的底线用例。读这类代码我一般会沿着 main.c 的调用顺序走一遍打开文件、逐 token 取、喂给 parser、拿回 AST、再交给 codegen。这个顺序捋顺了整套代码的骨架就清楚了。还有一个细节值得注意——Makefile 里的依赖顺序就是作者的真实设计意图先编译谁、后链接谁都写在里面比 README 可靠得多。很多人上来就扑到 lexer.c 里看状态机实现结果被一个无关紧要的字符跳过逻辑卡了半天这就是没先看整体结构。2.2 文法定义动手写代码前先把“规格”钉死不看文法就看不懂 parser.c这是读编译原理实验代码最大的门槛。课程实验一般会给一个简化 C 语言的子集文法用上下文无关文法描述长这样program - declaration_list declaration_list - declaration declaration_list | declaration declaration - type id ; | type id ( ) { stmt_list } type - int | void stmt_list - stmt stmt_list | epsilon expr - term add_op expr | term add_op - | - term - factor mul_op term | factor mul_op - * | / factor - number | ( expr ) | id读这段文法时要注意两点。第一每个产生式基本对应 parser.c 里的一个函数所以文法清单就是语法分析器的“函数清单”文法里有多少非终结符代码里就会有多少个解析函数。第二终结符集合就是 lexer.h 里那个 token_type 枚举词法分析器输出的每个 token 类型在文法里都能找到位置。我见过很多同学拿到实验代码直接查语法分析函数的实现查了半天不知道那个函数为什么要处理某个 token就是因为没把文法列出来对照。动手改代码前先把文法理解透所谓“理解透”是指你能对着文法把这门语言的合法程序写出来并且能指出哪些写法是文法不允许的。这一步省了后面改语法分析一定返工而且返工成本非常高——改文法比改代码还麻烦因为你要同时改 parser.c、AST 结构和测试用例三处。还有一个实用技巧在 parser.c 里搜“void match(int token)”之类的函数它负责消费一个 token 并做错误处理这个函数是理解所有分析函数的中枢。每个解析函数的开头几乎都是一次 match 调用看懂 match 怎么工作整个语法分析器怎么运转就明白了。2.3 实现语言选型C、Java、Python为什么课程实验八成用 C说个很现实的现象高校编译原理实验代码里C 语言是绝对主力。这里要提到“java编译原理”这个常见组合——确实有大量学校的实验是用 Java 写编译器的甚至不少教材的配套代码就是 Java 版。用 Java 写的好处是对象建模自然每个产生式对应一个类AST 节点用继承体系组织到语义分析阶段类型检查的代码可读性很高。但它的坑也明显容易在“类爆炸”上走偏建了一堆抽象工厂、访问者模式结果语法分析还没跑通时间全耗在重构上了。用 Python 写编译器的课程实验也越来越多好处是快词法分析用正则库一把梭几个小时就能出一个能跑的原型。坏处是到了代码生成阶段要输出几千行中间代码或目标汇编时Python 的性能和调试体验都不够舒服而且动态类型让“符号表存错类型”这种错误要到运行期才暴露。那为什么说课程实验代码八成用 C因为 C 语言的最小结构和编译原理的底层概念天然对齐结构体指针就是语法树节点之间的边链表就是符号表和中间代码序列指针的地址语义正好模拟“变量声明在栈上”这个模型。另外C 代码可以直接输出汇编风格的目标代码生成阶段不需要额外的运行时包装。如果你拿到的是一份 C 写的实验代码我的建议是别换语言原样改。换语言等于把“看懂并复现”这个任务升级成“重写一遍”工作量翻倍还不一定比原作好。选语言这件事本身没有绝对优劣但课程实验的评分标准往往看的是“功能是否完整、测试用例是否通过”稳定跑通比炫技重要得多。3. 用 C 语言手写词法分析器状态机、符号表和一个可运行的最小实现3.1 正则到状态转换表词法分析器的“第一性原理”词法分析器干的事情就一件把源代码字符串切成 token 流。课程实验里的 lexer 有两种写法一种是用 flex 生成另一种是手写状态机。手写状态机虽然累但对理解“正则表达式到底怎么工作”帮助最大所以很多课程实验代码里的词法分析器都是手写的至少主框架是手写的。手写状态机之前我习惯先画一张状态转换表把字符类别和状态的关系列清楚。比如识别标识符和关键字核心状态就三个开始状态、标识符状态、数字状态。这张表画出来之后写代码只是把表翻译成控制流而已当前状态输入字符下一状态动作S0字母 / 下划线S1开始拼标识符S0数字S2开始拼数字S0空格 / 换行S0跳过S1字母 / 数字 / 下划线S1继续拼S1其他S0结束标识符回退一个字符S2数字S2继续拼数字S2其他S0结束数字回退一个字符表驱动写法和 switch-case 写法在课程实验代码里都很常见。表驱动的好处是状态多了之后逻辑集中改一处就能改整个 token 的识别规则坏处是表格本身不容易读懂。我一般先用表想清楚所有路径再用 switch-case 写实现因为 switch-case 可以直接读代码追踪执行路径调试时下断点更直观。3.2 最小可运行词法分析器识别关键字、标识符和整数下面这段代码是从状态转换表直接翻译来的能识别关键字、标识符和整数是课程实验词法分析器的最小骨架。它没有处理运算符和字符串字面量但理解了这个结构往里面加任何 token 类型都是在既有分支里扩展。#include stdio.h #include ctype.h #include string.h typedef enum { TOK_ID, TOK_INT, TOK_KEYWORD, TOK_EOF } TokenType; typedef struct { TokenType type; char text[64]; int line; } Token; static const char *keywords[] {int, void, return}; int is_keyword(const char *s) { for (int i 0; i 3; i) if (strcmp(s, keywords[i]) 0) return i 1; return 0; } int next_char(FILE *fp) { return fgetc(fp); } Token next_token(FILE *fp) { Token t {0}; int ch; while ((ch next_char(fp)) ! EOF) { if (isspace(ch)) continue; if (isalpha(ch) || ch _) { // 标识符以字母或下划线开头 int len 0; while (isalnum(ch) || ch _) { t.text[len] (char)ch; ch next_char(fp); if (len 63) break; } t.text[len] \0; ungetc(ch, fp); // 多读的一个字符要放回去 t.type is_keyword(t.text) ? TOK_KEYWORD : TOK_ID; t.line 1; return t; } if (isdigit(ch)) { // 数字只处理十进制整数 int len 0; while (isdigit(ch)) { t.text[len] (char)ch; ch next_char(fp); } t.text[len] \0; ungetc(ch, fp); t.type TOK_INT; t.line 1; return t; } /* 运算符、分隔符、字符串字面量、错误字符分支略 */ } t.type TOK_EOF; return t; }这段代码有三个地方要注意。第一ungetc 是关键设计——读字符时不知道何时结束所以多读一个字符必须放回输入流否则下一个 token 会少一个字符。几乎所有手写词法分析器都必须处理“回退一个字符”。第二text 数组固定 64 字节超长标识符会被截断而不会报错这是个隐藏的问题真实的课程实验评分里会专门测这种情况后面避坑清单会展开讲。第三is_keyword 用线性查表只有三个关键字看起来没问题但如果关键字数量上到几十个就该换二分查找或哈希了。参数说明TokenType 枚举的顺序会影响调试输出建议把 TOK_EOF 放最后text 缓冲区的大小要跟词法规则里标识符的最大长度对齐line 字段现在是写死的实际实验里每次遇到换行要自增而且要在 token 结构里记录起始列号错误报告才好看。3.3 编译原理符号表从“存名字”到“存类型和作用域”词法分析器只负责切 token真正让变量有“身份”的是符号表。“编译原理符号表”这个概念在课程实验里经常被单独考因为它直接决定语义分析阶段的正确性。我见过最直观的符号表实现就是用链表模拟栈式作用域typedef struct Symbol { char name[64]; int type; // 0int, 1void, 2function int scope_depth; int line_declared; struct Symbol *next; } Symbol; typedef struct Scope { Symbol *head; // 本作用域内的符号链表 struct Scope *parent; // 外层作用域 } Scope;符号表在课程实验里不需要上哈希表链表完全够用。关键的设计是 Scope 结构体每个大括号块对应一层作用域。遇到声明时新符号插到当前作用域的链表头部查找变量时从当前作用域开始逐层向外找。作用域结束时直接把这一层的整条链表丢弃这模拟的就是“局部变量随函数返回而销毁”的语义。把这个结构写对需要想清楚三个问题作用域什么时候压栈、什么时候弹栈同名变量在不同作用域出现时查找顺序怎么保证内层优先全局变量和函数名放在哪一层。很多实验代码在这三个问题上的处理并不一样有的把函数名单独放在一个表里有的跟全局变量混在一起读代码时要先分清。我自己的习惯是把符号表的 dump 函数早点写出来遍历每一层作用域打印所有符号跑测试用例时用肉眼检查绑定关系比单步调试快得多。3.4 词法阶段最容易被忽视的一件事行号、错误恢复和“玄学”报错词法分析阶段看着简单翻车概率却很高。最经典的坑是报错行号不准整个 lexer 只用一个全局 line 变量但在回退字符时忘了回退行号或者多读了换行符没有处理导致语法分析器报错指到错误行人排在后面查半天。另一个常见问题是错误恢复做得太粗糙——lexer 遇到不认识的字符直接 abort而不是跳过一个字符继续扫描。课程实验的测试用例里经常混入故意写错的程序如果 lexer 一碰见错字符就崩溃后面的所有错误信息都出不来评分直接扣光。还有一类“玄学”问题明明语法分析器写对了但测试用例就是过不了最后定位到是词法层把“”拆成了两个“”token。手写词法分析器处理这种两字符运算符时必须先读第二个字符判断是不是组合运算符再决定回退几个字符。稍微不留神就会在这上面翻车。我把这些放在一起说是因为词法分析器的问题往往不会在词法阶段暴露而是拖延到语法分析阶段才爆发排查起来特别费劲。建议一开始就给 token 结构加 line 字段并且把“回退一个字符”做成统一函数所有分支都走同一套回退逻辑能省掉后面一大半调试时间。4. 语法分析器与中间代码生成从 LL(1) 到 LR(1) 的落地差异4.1 LL(1) vs LR(1)课程实验为什么默认选 LR语法分析是编译原理实验的硬核区域也是实验代码里最厚的一部分。课程实验里主流有两条路线手写递归下降对应 LL(1)和使用 Yacc/Bison 生成 LR(1) 分析器。这两条路线的取舍直接决定整套代码的结构风格。项目LL(1) 递归下降LR(1)Yacc/Bison文法左递归必须手动消除原生支持实现难度低按函数逐个写高需要理解移进-规约出错定位较差报错常靠后相对清晰改文法成本改函数逻辑较多改文法文本再重新生成课程实验采用率手写型实验常用生成器型实验常用两种路线的选择不是“哪个更好”而是“实验要求你掌握哪个”。手写递归下降能让面试和考试讲得出“预测分析表怎么构造”而 Yacc/Bison 生成的 LR 分析器在工程上更稳因为它自动处理了更多的文法冲突。课程实验代码里如果出现 .y 文件和 .lex 文件那就是生成器路线如果只有一个 parser.c那基本是手写递归下降。两种代码的读法完全不同手写版要顺着函数调用关系读生成版要读 .y 文件里的文法规则和语义动作。我见过不少实验代码是混着来的文法用 Yacc 写但语义动作里嵌了大量的 C 代码直接构建 AST 和符号表。这种类型的代码读起来最费劲因为你要同时理解 Yacc 的移进-规约过程和你自己 C 代码的执行时机。如果只是为了通过课程实验建议选一条路线走到底不要混。4.2 消除左递归后写递归下降表达式文法的一个完整片段手写递归下降的第一步是把左递归消除掉。表达式文法是最典型的例子原始文法里“expr - expr term”这种形式直接写递归会栈溢出。消除左递归后的表达式文法长这样expr - term expr_tail expr_tail - term expr_tail | epsilon term - factor term_tail term_tail - * factor term_tail | epsilon factor - number | ( expr )对照这段文法递归下降分析器的代码几乎是机械翻译每个非终结符写一个函数函数内部按产生式的右部顺序调用。关键在如何用向前看的一个 token 决定进入哪个分支int lookahead; // 当前 token由词法分析器填充 void match(int expected) { if (lookahead ! expected) error(syntax error, expected token %d, expected); lookahead next_token(); } int expr_tail() { if (lookahead TOK_PLUS) { // 匹配产生式 expr_tail - term expr_tail match(TOK_PLUS); term(); return expr_tail(); } return 0; // epsilon 分支不消费 token 直接返回 } int expr() { term(); // 匹配产生式 expr - term expr_tail expr_tail(); return 0; }这里的核心在于 lookahead 的维护。match 函数消费一个 token 后立即取下一个但取下一个的动作放在函数调用的边界处递归调用链才能顺畅。epsilon 分支的处理是递归下降的经典难点如果当前 token 不属于任何候选产生式的首字符集就认为匹配空了。这个设计对新手来说很容易写成“报错后不知道往哪返回”调试时建议在 match 函数里加打印把当前 token 的文本值和期望值都打出来一眼就能看出卡在哪个产生式上。4.3 建立抽象语法树把“匹配成功”变成“结构化数据”光有语法分析的成功还不够实验要的是能拿这棵树做后续处理。能跑通的语法分析器如果只是调用 match 就返回后面语义分析和代码生成就无从谈起。所以课程实验代码里一定有一个 AST 构建层常见的最小结构是这样typedef struct ASTNode { int kind; // 0number, 1binop, 2decl, 3funcall int op; // kindbinop 时: ADD1, SUB2, MUL3, DIV4 int value; // kindnumber 时有效 char name[64]; // kinddecl / funcall 时有效 struct ASTNode *left; struct ASTNode *right; } ASTNode; ASTNode *make_binop(int op, ASTNode *l, ASTNode *r) { ASTNode *n (ASTNode *)malloc(sizeof(ASTNode)); n-kind 1; n-op op; n-left l; n-right r; return n; }AST 构建是实验代码的“分水岭”语法分析只负责判断程序合不合法AST 才记录了程序“长什么样”。读实验代码时先看 ASTNode 这个结构体比看 parser.c 的实现更有用。为什么因为 ASTNode 的字段决定了作者认为这门语言里“哪些信息值得保留”比如声明节点的 name、表达式的 op、数字字面量的 value这些字段就是后面语义分析和代码生成能拿到的全部信息。我在实际调试中会单独写一个打印函数把 AST 树按中缀表达式的形式打出来比如输入是“123”打印结果应该还是“123”。如果打印成了“(12)*3”说明 AST 构建时左右子树挂错了问题一定出在 factor 函数对括号的处理上。这种用“反向打印”验证 AST 正确性的方法比对着结构体一个个字段看快得多。4.4 三地址码中间代码生成的课程实验版中间代码生成是课程实验的“最后一公里”很多实验到这里就收尾了。三地址码是一种常见的中间表示它的每条指令至多有一个运算符和三个地址课程实验里用四元式保存就足够了typedef struct Quad { char op[8]; // ADD, SUB, MUL, DIV, ASSIGN, LABEL, GOTO char arg1[32]; char arg2[32]; char result[32]; struct Quad *next; } Quad;表达式“a b * c”翻译成三地址码大致是t1 b * c t2 a t1 a t2代码生成函数是递归下降的镜像遍历 AST 的每个节点把表达式树转换成线性指令序列。临时变量从 t1 开始编号每个二元运算的中间结果都分配一个新的临时变量。递归到叶子节点时如果是变量就直接用变量名如果是数字就生成一个常量赋值。函数调用、数组、指针这些高级特性在三地址码这一层都会变成额外的指令类型但课程实验通常只需要支持到表达式和简单语句。中间代码生成的常见错误跟寄存器分配无关因为还没有到目标代码层。真正的坑在于临时变量的生命周期很多人直接用一个全局计数器分配编号但在生成了循环语句标签跳转之后临时变量可能跨越多个基本块这时编号策略会直接影响后面做寄存器分配或者栈式存储的正确性。我一般会在三地址码生成阶段把每条指令的源操作用一个统一的“get_temp”函数申请这样回头要追溯某个临时变量的定义点只要搜这个函数就行。中间代码这一层是后面所有优化的入口代码写规范一点后面做扩展实验能省很多事。5. 编译原理实验代码的避坑清单5 条能把人逼疯的典型翻车点5.1 现象make 直接报错报的却是库函数找不到这是拿到实验代码后第一个容易翻车的地方。明明代码完整一敲 make 就输出一堆“undefined reference to yylex”或者“implicit declaration of function fgetc”。原因通常有两个一是实验代码是在旧版 GCC 环境写的新版编译器默认启用了更严格的检查二是机器上压根没装 flex/bison 开发库链接时找不到 lex 相关的函数库。解决的办法是先看 Makefile 里的 CC 和 LDFLAGS。如果代码用了 flex需要安装 flex 和 bison 的开发包Ubuntu 下是sudo apt install flex bison libfl-dev。如果是警告被当成错误最简单是在 CFLAGS 里加-w屏蔽警告不要试图在一个下午里把旧代码的所有警告都修干净。我处理这类问题的顺序是先确认依赖库装没装再看 CFLAGS最后才看源码。很多同学第一步就打开 lexer.c 开始改代码改了半天发现根子在链接参数就是没按这个顺序排查。5.2 现象词法分析把“1e-3”识别成“1”和“e - 3”写测试用例时发现科学计数法的数字被拆成了好几个 token“1e-3”变成了数字“1”、标识符“e”、减号、数字“3”。原因主要是词法分析器里的数字状态机只处理了十进制整数没有处理指数部分。解决的方法是在数字分支里显式处理 e/E 和后继的整数。具体步骤在 isdigit 循环结束后判断下一个字符是不是 e 或 E如果是再往后看一位是不是数字或符号是就继续吞字符不是就把这个字符回退。这一套逻辑听上去不复杂但很容易和“标识符以 e 开头”的情况冲突比如变量名是“e”。所以状态机必须在“指数部分必须有至少一个数字”的前提下才能把 e 并入数字 token。先把这个条件想清楚再动代码不然会从“1e-3 识别错”变成“变量 e 被识别成数字”问题只是换了位置。5.3 现象语法分析一跑就死循环看起来像卡在某个产生式里调试语法分析器时最崩溃的场景程序跑起来后没有任何报错但就是不结束CPU 占用 100%。根源多半是文法的左递归没有消除干净。递归下降分析器遇到“expr - expr term”这种产生式会无限递归调用 expr 函数每次调用都试图消费同样的 token但 token 流没有前进于是死循环。解决的办法是先检查文法里所有递归产生式凡是形如“A - A α”的左递归都必须改写成“A - β A”和“A - α A | ε”。如果是用 Yacc/Bison 生成的 LR 分析器左递归文法本来支持不会死循环但手写递归下降就必须严格消除。我排查这类问题时会先在每个解析函数入口加一行打印输出当前函数名和 lookahead token一运行就能看到哪个函数在反复进入且 token 没有变化。这个方向几乎百发百中不需要读完整份代码就能定位。5.4 现象符号表作用域串味两个同名变量后面的引用绑定了最早那个语义分析阶段最常见的 bug是我自己当年也栽过的写了两个函数各自有一个局部变量 i结果函数 A 里引用 i 时符号表查出来的却是函数 B 里那个 i 的地址。原因是符号表只做了链表的插入没有做作用域弹出——所有符号都堆在同一个链表里查找时只按名字匹配不区分作用域。解决的办法是给符号表加上作用域层次。进入一个大括号块时新建一个 Scope 压栈声明的新符号都挂到当前 Scope 的链表上作用域结束时把当前 Scope 整层丢弃。查找时从当前层逐层向上找找到就返回不继续向外。这个设计其实就是第 3 章那个 Scope 链表的完整形态把 scope_depth 字段打印出来一眼就能看出符号到底挂在哪层。这个坑的麻烦之处在于大多数测试用例里同名变量不常见小规模样例全过一上综合测试就崩而且崩的位置离声明位置很远没有作用域打印的话很难定位。5.5 现象代码生成阶段一“优化”就翻车生成的代码算错数很多同学做完中间代码生成会尝试做一个简单的寄存器分配优化把临时变量直接映射到寄存器上。结果一跑测试用例原来能算对的表达式优化后结果变成了随机数。常见原因是同一个寄存器被多个变量复用了或者临时变量在循环里被覆盖后又被引用。解决的办法是先把“不优化”的版本跑正确所有局部变量都分配在栈上临时变量也放栈里寄存器的使用限于表达式求值的短暂过程。等这个版本完全通过测试再动手做寄存器分配。把性能优化的优先级放在正确性之后这是我一贯的原则。更准确地说优化是给已经跑得通的代码准备的不是给跑不通的代码补救的。实验代码的评分首先看正确性在正确性没保证之前优化只会让错误更难找。先从最简单的情形开始比如一个函数内不超过 N 个临时变量时都用寄存器超过就回退到栈这样的增量优化路径比一次性全量做要安全得多。6. 把实验代码盘活的三板斧测试用例、可视化 AST 和扩展方向6.1 测试用例集怎么设计比“能跑”更重要的“对错可见”课程实验最难的不是写编译器而是证明你的编译器是对的。我习惯把测试用例分成三层基础用例覆盖声明、赋值、算术表达式、函数调用边界用例覆盖空程序、超长标识符、多层括号嵌套、连续运算符错误用例覆盖语法错误、未声明变量、类型不匹配。每一层用例都要有明确的预期输出比如表达式求值结果、符号表 dump 内容、语法报错行号。把预期输出固化成文件每次改动后跑一遍对比比肉眼盯着屏幕看靠谱得多。6.2 用 Graphviz 把 AST 画出来调试语法分析最好的工具就是把 AST 画出来。Graphviz 的 dot 格式表达树形结构只需要两样东西节点定义和边定义。写一个遍历函数把每个 AST 节点输出成一个 dot 节点再把父子关系输出成边保存成 .dot 文件用一行命令渲染成图片dot -Tpng ast.dot -o ast.png我看一棵“12*3”的 AST 图立刻就能看出乘法节点是不是在加法节点的下面优先级对不对一目了然。这一步用几分钟就能搭好但调试效率能提高一个量级。做这个工具时注意给每个节点编号避免两个内容相同的节点在 dot 图里被合并。6.3 一条扩展路线给 MiniC 增加 for 循环如果实验代码已经能跑通基本功能我建议把它做成自己的扩展实验。最常见也最划算的扩展是给 MiniC 增加 for 循环。需要改动的位置正好是整套编译器的全链路文法定义、词法 token、AST 节点、中间代码生成和符号表作用域。for 循环的三地址码风格大致是label L1 if i n goto L2 goto L3 label L2 ... 循环体 ... i i 1 goto L1 label L3把这条链路完整走一遍等于把整个编译器重新复习了一遍比再做一套新题有价值得多。我做这类扩展的经验是每次只加一个特性先保证旧测试用例全过再写新用例验证。加完 for 之后再尝试 while、break、continue难度梯度非常合适。我现在的习惯是拿到任何一份编译原理实验代码第一件事不是打开 src 而是打开 testcase 目录先把用例跑通再倒回去读代码。读代码时只看 token_type、ASTNode、Quad 这三个结构体其余实现细节全部往后放。这套方法帮我从“背实验报告”变成了“真的能改代码”希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询