☰
语法分析程序设计与实现:从递归下降到Bison的避坑指南
2026/10/22 1:09:14 网站建设 项目流程

简介:一份面向编译原理课程实验二的语法分析程序设计与实现文档,适合计算机专业学生及需要完成词法/语法分析综合实验的初学者。文档以算术表达式简化子集为分析对象,给出原始BNF定义及改写后的LL(1)文法,完整涵盖实验目的、程序流程图、预测分析表构建、C++源程序实现,以及RIGHT/ERROR输出等测试要求,并附有LL1分析表填充和关键数据结构定义。资源为单个doc文档,共1个文件,压缩包大小127KB,内容以可运行的LL(1)预测分析程序为核心。已有143人学习下载,适合作为编译原理课设、实验报告撰写或复习参考。文档对预测分析过程、产生式结构体、分析栈与剩余串输出等关键细节均做了说明,读者可以据此理解自顶向下语法分析方法,并能直接参考或调试代码,快速完成同类实验。

1. 为什么都到实验二了,还有人对着黑框里的一堆报错发呆

语法分析程序设计与实现,是几乎每门编译原理课里最劝退的一道坎。实验一写完词法分析器,你手里好歹还有一堆token流可以打出来看;到了实验二,要面对的是文法、FIRST集合、FOLLOW集合、预测分析表、递归下降、LR(0)、SLR(1)这些名词,程序跑起来不是死循环就是满屏syntax error,连错在哪儿都看不出来。这个标题看起来只是个课程文档里的章节名,实际背后是一整套「从文法到可运行分析器」的落地路径,理解了这条路,后面做语义分析、中间代码生成都是顺着同一套思路往下走。这篇文章不聊理论书上的定义,直接说清楚你拿到题目后该怎么选方案、怎么把文法变成能跑的代码、参数的坑在哪,以及不同写法之间差在哪里。

2. 先把两类分析路线看清楚:自顶向下与自底向上的取舍依据

2.1 输入、输出与中间产物:别急着写代码,先画这条数据流

语法分析器的输入是词法分析器产出的token流,输出是两类东西:一棵语法树(或者至少是一个能证明输入合法的推导序列),以及一个清晰可读的错误报告。很多同学一上来就对着文法写函数,写着写着发现token流里混进了空白符、注释符,或者词法接口返回的是字符串而不是类别编码,整个分析器瞬间失控。我一般会先把接口定死:token至少包含两个字段,一个是类别编号(用枚举常量),一个是语义值(数字的值或者标识符的字符串)。语法分析器只认类别编号,不直接碰源代码字符,这是第一条纪律。

常见做法是让词法分析器暴露一个函数,比如next_token(),每次返回一个token;语法分析器内部再维护一个「当前token」的缓存,叫做lookahead。所有判断都基于lookahead的类别编号去分支,而不是去读词法缓冲区的原始内容。这样设计的好处是:语法分析的逻辑完全与具体语言的关键字、标点符号的字面值解耦,你想换成另一种词法实现,语法层代码一行都不用改。

另一个容易被忽略的中间产物是推导序列。如果你做的是自顶向下分析,每次用哪个产生式展开,记录下来就是最左推导;自底向上分析每次规约哪个句柄,倒过来就是最右推导。实验报告里要求的「分析过程输出」,本质就是把这两个序列打印出来。很多人把分析器和语法树混成一件事,其实在递归下降里你完全可以在匹配成功时不建树,只打印产生式编号;在LR分析器里也可以只打印移进/规约动作表。先跑通这个最小路径,再谈建AST,调试成本会低很多。

2.2 手写递归下降与生成器选哪种:看题目给了什么文法,不看你喜欢什么

自顶向下和自底向上不是风格问题,是文法的约束条件决定的。递归下降要求文法不能有左递归,最好还能通过提取公共因子避免回溯;LR分析对文法的要求更宽,很多左递归文法(比如expression: expression '+' term)是天然适合LR的,因为LR用移进规约的方式天然处理了左递归,不会像递归下降那样无限递归栈溢出。如果你的题目给了一堆BNF,里面还有大量左递归产生式,你硬改成递归下降就得先做文法改写,引入非终结符E'之类,这一步是纯机械操作,但出错率极高。

我的选型标准很朴素:如果题目只要求识别一个表达式语言(加减乘除、括号、赋值),递归下降完全够用,代码量小,调试直观,出问题看函数调用栈就能定位;如果题目要求识别一个含有if-else、while、函数调用、声明列表的真实语言片段,或者明确要求用LR(1)/SLR(1)方法,那别头铁,直接用Bison或Yacc这类生成器。生成器能自动处理冲突检测,还会告诉你哪里有shift/reduce冲突,帮你把文法里的二义性揪出来——这是手写分析器永远做不到的。

参数和工具链上也有差别。递归下降是纯C/C++代码,跟着你的词法器一起编译链接,毫无额外依赖;生成器路线需要装bison、flex这类工具,生成出来的.c文件再和你手写的词法接口拼到一起。很多人在这一步翻车:bison生成的.c文件在Windows上用MinGW编译要加-D__USE_MINGW_ANSI_STDIO之类的宏定义,或者flex生成的词法器在C++工程里会报yywrap未定义。这些坑不是你的分析器写错了,是工具链配置问题,提前确认总没错。

2.3 FIRST集合与FOLLOW集合是选路的地图:算错一个就全盘走偏

无论你走哪条路,FIRST和FOLLOW集合都是绕不过去的地基。自顶向下构造预测分析表要用这两个集合决定在某个非终结符面对某个输入符号时选哪条产生式;自底向上的SLR(1)自动机构造也要用FOLLOW集合来填规约项的lookahead。很多同学拿现成工具算这两个集合,结果手算文法题时算错,连带着把程序里的硬编码表也写错,跑起来表现成「词法没问题、文法也简单,但就是某些合法输入被误报」。

建议的做法是:先针对题目给的文法,手工把FIRST和FOLLOW集合算一遍,然后写一个很小的脚本去校验,或者用已知正确的公开实现跑一遍对照。我给一个最小文法做示范:

E -> E + T | T T -> T * F | F F -> ( E ) | id

这是典型的左递归算术表达式文法。先消除左递归得到:

E -> T E' E' -> + T E' | ε T -> F T' T' -> * F T' | ε F -> ( E ) | id

这时再算FIRST集合就不容易漏了。比如FIRST(E')里的ε,FOLLOW(E')里的'+'和')',这些细节就是后面判断「空产生式什么时候启用」的直接依据。如果题目允许你用LL(1)分析法,那还需要把每个非终结符对每个终结符的预测表项逐一填出来,冲突出现的地方就是文法还没改造干净。这些计算步骤别偷懒,实验报告里这部分占了大量分值,而且它直接决定了你的代码里查表逻辑怎么设计。

3. 手写递归下降分析器:从文法到可运行代码的全过程

3.1 先做文法改写:消除左递归与提取公共因子,这一步决定后面少改多少代码

递归下降要求每个非终结符对应一个函数,函数主体按产生式的右部顺序匹配终结符或调用其他非终结符。如果文法里有左递归,比如E -> E + T,函数parse_E的第一行就去调用parse_E,无限循环,程序直接栈溢出。所以第一步是改写文法,上面那个算术表达式文法改写后的结果就可以直接用。提取公共因子的场景也很典型,比如statement -> if (expr) stmt | if (expr) stmt else stmt,两个产生式前缀完全相同,直接写函数就会出现不确定选哪条的情况。改写成statement -> if (expr) stmt else_part,把公共前缀提出来,else_part允许为空,问题就解决了。

改写文法最重要的是保持语言不变,就是新旧文法接受的语言集合必须等价。你别把运算符的结合性改反了——消除左递归后,E' -> + T E' | ε表达的是左结合,因为递归发生在+号右边对应的是外层;如果你改成E' -> T + E',那表达式的含义就变成了右结合,2+3+4会解析成2+(3+4),虽然结果数值一样,但语义上错了,到了实验三语义分析阶段就会埋雷。做完改写,我习惯先把几个典型输入(比如id+idid、id(id+id))用手推导一遍,确认推导序列符合预期,再开始写代码。

3.2 核心代码框架:match、next_token、parse_E三个函数的写法与参数含义

下面给一个最精简的递归下降分析器骨架,词法接口假定为get_token()返回一个Token结构体,字段type是枚举类型编号,value是整数值或字符串指针。为节省篇幅,我用一个简化的枚举常量示例,实际项目中你按自己实验一的定义替换即可。

#include <stdio.h> #include <stdlib.h> // 假定这是实验一里定义的token类别 enum TokenType { TOKEN_ID, TOKEN_NUM, TOKEN_PLUS, TOKEN_MUL, TOKEN_LPAREN, TOKEN_RPAREN, TOKEN_ASSIGN, TOKEN_SEMI, TOKEN_EOF }; typedef struct { int type; int value; // 数字的值,或标识符的符号表下标 char *lexeme; // 原始字符串,调试用 } Token; // 词法分析器接口:每次返回下一个token extern Token get_token(void); // 全局当前token,作为向前看符号 static Token lookahead; // 错误计数,超过阈值可以提前终止 static int error_count = 0; static void next_token(void) { lookahead = get_token(); } static void report_error(const char *msg) { fprintf(stderr, "syntax error: %s, near '%s'\n", msg, lookahead.lexeme); error_count++; } // match函数:期望一个指定类型,匹配则消费,否则报错 static void match(int expected) { if (lookahead.type == expected) { next_token(); } else { report_error("unexpected token"); // 这里不做恢复,后续函数会继续用当前token判断 } }

这段代码里,lookahead这个全局变量是整个分析器的状态核心,所有parse函数读取它做分支判断。match函数是唯一的token消费入口,你只要保证所有终结符都通过match去消费,就不会出现token流错位的问题。error_count是错误恢复的基础参数,你可以设定一个上限,比如超过10个错误就终止分析,避免错误级联导致程序输出几千行无效报错。

接下来是表达式文法对应的三个解析函数。文法规则是:E -> T E';E' -> + T E' | ε;T -> F T';T' -> * F T' | ε;F -> ( E ) | id。注意这里的T和E'每个对应一个函数,连空产生式也要写成一个分支:

static void parse_E(void); static void parse_E_prime(void); static void parse_T(void); static void parse_T_prime(void); static void parse_F(void); static void parse_E(void) { // E -> T E' parse_T(); parse_E_prime(); } static void parse_E_prime(void) { // E' -> + T E' | ε if (lookahead.type == TOKEN_PLUS) { match(TOKEN_PLUS); parse_T(); parse_E_prime(); // 递归调用处理连续加号 } // 否则,产生式选择空串,直接返回 } static void parse_T(void) { // T -> F T' parse_F(); parse_T_prime(); } static void parse_T_prime(void) { // T' -> * F T' | ε if (lookahead.type == TOKEN_MUL) { match(TOKEN_MUL); parse_F(); parse_T_prime(); } } static void parse_F(void) { // F -> ( E ) | id if (lookahead.type == TOKEN_LPAREN) { match(TOKEN_LPAREN); parse_E(); match(TOKEN_RPAREN); } else if (lookahead.type == TOKEN_ID) { match(TOKEN_ID); } else { report_error("expected factor"); } }

逻辑说明:整个分析过程是对输入串的最左推导模拟,parse_E调用parse_T,parse_T调用parse_F,遇到数字或括号就匹配终结符;加号和乘号分别由E'和T'函数处理,递归的写法天然支持任意长度的运算链。这个结构的巧妙之处在于,E'的递归调用产生的是循环效果,但不用写循环语句,回溯栈由函数调用栈自然承载。

参数说明:如果你要支持减法、除法,只需增加对应的终结符类型和分支;如果题目要求负号(一元减),需要给F增加一个分支match(TOKEN_MINUS)后接parse_F。但有一个关键参数你必须注意:parse_E_prime里只有遇到PLUS才匹配,遇到别的符号就返回,这意味着如果输入是id id这种缺运算符的表达式,分析会在parse_E_prime直接返回后,由更高层去发现token没消费完。所以你的入口函数在parse_E之后必须检查lookahead.type == TOKEN_EOF,否则一个"id +"的错误输入也可能被误判为合法。

3.3 三个必调的参数:前瞻符号、错误上限、恢复点

第一个是向前看符号的数量。上面这个文法只用了一个lookahead,属于LL(1)。如果题目给出的语言无法在1个前瞻符号下做决定,比如C语言的类型声明和表达式语句冲突,就必须增加前瞻或者改写文法。在递归下降里,增加前瞻的写法是再提前拿一个token缓存起来,比如lookahead2,分支判断时两个联合判断,代码复杂度会明显上升。我的建议是:优先改文法,而不是加前瞻,因为前瞻数量一多,出错的组合呈指数级增长。

第二个是错误计数上限。error_count这个变量不只是用来统计,它应该参与控制流程。常见做法是在每次report_error后检查error_count,超过比如5次就直接exit(1),因为此时已经基本确定输入不合法,继续跑只会浪费时间和打出一堆不可信的报错。有的同学觉得多报几个错显得「分析器很努力」,实际上老师在验收时看到满屏报错反而会怀疑你的错误恢复逻辑没做。

第三个是错误恢复点。最简单的panic mode是:当match失败时,跳过当前token直到遇到一个同步符号(通常是分号或右括号),再继续分析。这个同步符号集合怎么选?直觉上是选FOLLOW集合里的符号。比如在分析语句时,分号是同步点;在分析表达式时,右括号和分号都是同步点。代码实现就是在report_error后加一个while循环,判断lookahead.type不在同步集合里就next_token。注意这会造成一些合法token被跳过,但作为实验二的要求已经足够了,你要是追求更高端的错误恢复,参考最后一章的做法。

4. 用生成器(Bison/Yacc)实现语法分析:文件格式与联编细节

4.1 生成器文件的三段式结构:声明段、规则段、程序段各自写什么

如果题目明确要求用Yacc/Bison,或者文法里有很多左递归产生式让你改写得很痛苦,直接用生成器是明智选择。一个.y文件由四段组成,用%%分隔。第一段是声明段,写%token定义、%left/%right优先级声明、头文件包含;第二段是规则段,写各非终结符的产生式,每个产生式后面可以跟一段C动作代码;第三段是程序段,写主函数、词法分析器的调用、错误处理函数yyerror等。

一个典型计算器语法文件的声明段长这样:

%{ #include <stdio.h> #include <stdlib.h> int yylex(void); void yyerror(const char *s); %} %token TOKEN_NUM %token TOKEN_ID %left TOKEN_PLUS %left TOKEN_MUL %%

%left声明运算符的结合性和优先级是很关键的一个参数。这里的顺序是:越靠下的声明优先级越高,TOKEN_MUL写在TOKEN_PLUS的下面,所以乘法的优先级高于加法。同一行的%left表示左结合。如果你的文法里还有取负号这种一元运算符,需要写%right或者用%prec指令指定一个虚拟token来标记优先级,否则容易出现一元负号和二元减法的冲突。

规则段的写法相比手写递归下降,最大的区别是产生式直接写,不需要手动消除左递归。因为Bison生成的是LR分析器,天然支持左递归。比如算术表达式可以直接写:

expr: expr TOKEN_PLUS term { $$ = $1 + $3; } | term { $$ = $1; } ; term: term TOKEN_MUL factor { $$ = $1 * $3; } | factor { $$ = $1; } ; factor: TOKEN_NUM { $$ = $1; } | TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; } ;

注意每一个产生式后面的{ $$ = ...; }是语义动作,$$表示这条规则归约后得到的值,$1、$3表示引用右部第1个、第3个符号的值。这里的值类型由声明段的%union和%type指令决定。如果只做语法分析不建AST,你可以把这些动作留空或者只打印一条规约信息;一旦要建语法树,就在动作里创建节点、把子节点指针用$$传上去。

4.2 生成、编译与联调的最小命令序列

写完.y文件后,下一步是生成C源码并编译。以Bison为例,最小命令链是:

bison -d parser.y # 生成 parser.tab.c 和 parser.tab.h gcc -c parser.tab.c -o parser.tab.o gcc -c lexer.c -o lexer.o # 自己实现的词法函数 gcc parser.tab.o lexer.o -o parser

-d参数要求生成头文件parser.tab.h,里面定义了每个token的编号宏,词法分析器必须包含这个头文件,并返回与之一致的宏值。比如说你在.y里声明了%token TOKEN_NUM,Bison就会在parser.tab.h里定义#define TOKEN_NUM 258之类的宏,词法函数返回这个宏值,语法层的匹配才能对上。不包含头文件的典型症状是:编译没问题,运行时报warning,或者所有token被配成0导致永远走第一条产生式。

联调时最容易踩的坑是yylex函数和yyparse函数的交互。yyparse是Bison生成的入口,它每需要一个token就调用一次yylex(),你的词法函数必须至少实现一个全局变量yytext(Bison的c语言模式下),它是当前token的词素字符串。如果你的词法函数返回数字类型时还应该把数字值赋给yylval,否则语义动作里$1取到的是垃圾值。我见过很多次这样的场景:语法分析完全正确,优先级也对,但算出来的结果全是乱码,原因就是词法器里忘了写yylval = atoi(yytext)。

4.3 冲突报告怎么看:shift/reduce与reduce/reduce的处理策略

Bison生成时会输出冲突警告,比如State 12 conflicts: 1 shift/reduce。这两个词很有迷惑性,很多同学以为程序跑起来没问题就可以忽略。实际上,凡是报告里出现conflicts就说明你的文法是二义的或者有边界条件没写清楚,Bison在冲突点默认选择了shift,这不一定符合题目语义。比如经典的悬空else问题,就是shift/reduce冲突,Bison默认选择shift,恰好匹配了「else就近匹配」的预期,所以很多人误打误撞觉得没问题。但如果冲突发生在运算符优先级上,比如一个文法既没有声明%left也没有写%prec,那默认shift会让表达式变成右结合,算出来的结果就是错的。

处理冲突的标准步骤是:先用bison -v生成.output文件,里面能看到每个状态的详细项目集,找到冲突的具体项目和lookahead集合,再回到文法去找是哪两个产生式在同一个状态里相遇了。然后决定:要么调整优先级声明,要么改写文法消除冲突。reduce/reduce冲突更麻烦,它通常表示两个产生式能规约到同一个非终结符的同一个状态,原因是文法有重复路径,往往是提取公共因子没做干净。我的经验是,只要你看到reduce/reduce冲突,基本可以断定文法设计有问题,修文法比在代码里绕更快。

5. 实验二避坑指南:这些坑我踩过,你直接绕开

5.1 递归下降跑出无限递归栈溢出:左递归没清理干净

现象:程序一跑就崩溃,报错信息大量Stack overflow,或者进程卡死不动,CPU占用百分之百。这种问题在第一轮调试里出现频率最高。原因:写递归下降时直接用了题目原文法,里面包含E -> E + T这种左递归产生式,parse_E函数第一行就调用parse_T,parse_T走完又回到parse_E,形成一个永不退出的循环。解决:先做文法改写——把左递归转成右递归,引入新的非终结符E',改写完再多读几遍确认没有漏改。另外一个隐藏入口是「间接左递归」,比如A -> B x,B -> A y,这种也需要整体改写,不能只看每条产生式本身。

5.2 简单的合法表达式被判非法:FIRST集合里的空串没处理

现象:输入id+(id)能过,但输入id+id却报错,或者反过来。原因大多出在parse_E_prime这类函数对空产生式的处理上:当lookahead既不是+也不是*时,代表选择空串,直接返回。可如果你的判断条件写成了if (lookahead.type == TOKEN_PLUS) { ... } else { report_error(...); },那就把合法的结束情况也当成错误了。解决:先手工列出每个非终结符的FIRST集合,看看ε在不在里面,然后检查代码里对应的函数是不是「遇到不在FIRST集中的token就静默返回」。注意别把错误分支和空串分支混在一起写,至少要在注释里区分这两种情况。

5.3 匹配完所有token后没有检查EOF,非法输入漏判

现象:输入id+id然后后面随便黏一个非法字符,比如id+id@,分析器居然报告成功。原因:入口函数只调用了parse_E,匹配完E对应的token就返回了,后面的@字符根本没人管。解决:在入口函数里parse_E()之后再match(TOKEN_EOF),确保整个token流被完整消费。这招虽然简单,却是验收时最容易被扣分的地方,因为测试用例里往往会混入这类「前缀合法、整体非法」的输入。另一个相关习惯是:在match里对TOKEN_EOF也要做检查,避免分析器越过缓冲区末尾继续读。

5.4 Bison动作代码里$n引用错位,导致语义值错乱

现象:语法分析全部通过,程序输出结果不对,比如1+2*3算出9而不是7。这跟优先级冲突一样会造成错误优先级行为。原因:一种情况是%left声明顺序写反,乘法写在加法上面导致乘法优先级反而低;另一种情况是动作代码里$N数字写错,比如expr: expr TOKEN_PLUS term动作里想引用第三个符号却写成了$2,拿到的就是加号token对应的值,而token本身的值类型往往是NULL或者0。解决:先把所有动作里的$$打印出来调试,用printf("%d\n", $1); 逐层追踪;确认类型声明没问题的前提下,检查$N下标是1开始计数的,而不是0。Bison不对$N做类型检查,写错下标编译照样过,运行才炸,这是最坑的地方。

5.5 错误处理后状态机不同步,连续报一堆假错误

现象:输入第一个错误时,比如缺了一个分号,程序报错后就开始乱套,原本合法的后续语句也全被报错,最后错误计数爆炸式增长。原因:match失败后没有做任何恢复动作,当前lookahead还停留在错误位置,后续函数基于这个错误的token继续推导,自然全线崩溃。解决:加上panic mode恢复。给每个主要分析函数定义一个同步符号集合(通常是分号、右括号、EOF),在report_error之后循环跳过token直到遇到同步符号,然后再回到正常的分析流程。注意同步之后要调用next_token把同步符号也消费掉,否则会卡在同一个位置死循环。

5.6 中文路径或文件名编码导致bison生成失败

现象:在Windows上把.y文件放在带中文或空格的路径下,运行bison指令后生成的是乱码文件,或者干脆提示无法创建文件。原因:老版本bison对非ASCII路径支持很差,flex同样有这个问题。解决:把整个工作目录改成纯英文路径,比如D:\compiler\lab2,文件命名也别带空格。还有一个连带问题:如果代码文件里用了中文字符串作为错误提示,在MinGW编译时可能因为编码格式不对导致编译警告,最好统一用英文提示,或者代码文件用UTF-8 without BOM保存。

6. 进阶技巧:在语法分析的同时直接构建AST,以及让错误恢复更体面

到这里,你已经能跑通一个语法分析器了。但实验二如果只满足于「判断合法/非法」和「打印推导序列」,拿到高分很难,因为你没跟后面的语义分析实验接上。我通常会建议在语法动作里直接构建抽象语法树(AST)。递归下降的写法是在每个parse函数里返回一个节点指针,把子节点的指针拼成父节点;Bison的写法就是我在4.1节里写的,每个产生式的动作里用malloc建节点,子节点指针赋给$1、$3这类占位符,父节点通过$$传递。

以四则运算表达式为例,AST节点定义可以长这样:

typedef struct ASTNode { char op; // 运算符:'+'、'*'、'n'表示数字,'i'表示id int value; // 数字节点的值 char *name; // id节点的名字 struct ASTNode *left; struct ASTNode *right; } ASTNode;

在parse_E_prime里,原来只是匹配加号后递归,现在改成:

ASTNode *left = parse_T(); if (lookahead.type == TOKEN_PLUS) { match(TOKEN_PLUS); ASTNode *right = parse_T(); ASTNode *node = malloc(sizeof(ASTNode)); node->op = '+'; node->left = left; node->right = right; // 还要接着处理 E' 里的剩余部分,这时的当前节点不是上面的node // 而是需要向下传递,所以最好把parse_E_prime的参数改成传入左子树 }

这里有个编程细节:因为E'是递归调用,递归处理剩余的加号链时,新建的加号节点要把之前已构建的左子树作为left,当前解析出来的term作为right。换句话说,parse_E_prime的签名应该改成parse_E_prime(ASTNode *left_so_far),返回值是处理完后续所有加号的最终根节点。你如果沿用无参数的版本,就会丢掉中间节点,最后建出的树是扁平的,而不是左子树嵌套的形态。

错误恢复方面,除了同步符号恢复,你还可以做第二个级别的设计:记录错误发生的位置(行号列号)和期望的token集合。这部分做好了,实验报告的「错误处理机制」栏目就能写得比别人厚实。做法是在match失败时,除了打印unexpected token,再遍历一个预定义的「期望集合」,把当前状态下合法期望的所有token类型列出来,拼进报错信息里,比如在表达式开头期望的是数字或左括号。

最后说一个我自己的习惯:每次写完分析器,我除了跑老师给的测试样例,还会专门写一个生成合法/非法输入的小脚本,合法与非法各备二十来条,把边界情况覆盖一下——空输入、只有一个数字、嵌套括号十层、连续的加号、运算符后缺操作数、表达式后跟垃圾字符。这些测试看着不起眼,但它们帮我挡下了至少一半以上的验收翻车。走到这一步,你会发现语法分析的设计与实现真正考验的是「把一棵树从文字里长出来」的思维方式,代码本身倒不是瓶颈。希望这篇笔记能帮你把实验二这关过得顺利一些。

本文还有配套的精品资源,点击获取

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

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

立即咨询