☰
Flex+Bison实现Cminus词法语法分析,编译原理大作业实战
2026/10/13 0:35:40 网站建设 项目流程

简介:这是一份编译原理课程设计完整实现,面向计算机相关专业在校生、教师及企业开发者,演示如何利用Flex与Bison对Cminus语言进行词法分析与语法分析。压缩包共14个文件、体积约328KB,核心资源包含lexical.l词法规则、syntax.y文法定义,以及由工具生成的lex.yy.c与syntax.tab.c/h,另有main.c、多个input测试用例、README说明和实验报告文档,源码与文档搭配合理,便于按模块研读与复现。已有106人学习下载。项目代码经多个测试样例运行验证,功能完整,可作为课程作业、本科毕业设计或编译原理实验的起步模板。除基础分析流程外,读者还可利用现有框架扩展语义分析或中间代码生成,也可直接用于答辩展示与二次开发,适合需要快速理解Flex/Bison协作机制的学习者。

1. 编译原理大作业选型:Flex、Bison 和 Cminus 的三角组合到底解决什么问题

很多同学把编译原理大作业做成“抄代码”,真正拿到 Flex 和 Bison 去分析 Cminus 时,第一步就卡在 yylex 和 yyparse 到底谁先跑、Token 怎么传给 Bison、报错行号为什么永远对不上。这篇文章不是复刻某个神秘源码包,而是把一套能直接交差的 Cminus 词法分析 + 语法分析方案从头讲清楚:从 .l 文件里的正则规则,到 .y 文件里的 LALR(1) 文法,再到主程序怎么把两个工具缝合起来。适合正在赶大作业、需要快速跑出一个能识别 Cminus 程序并输出语法树的同学,也适合想搞懂 Flex 和 Bison 协作边界的人。

2. 先写词法:用 Flex 把 Cminus 的 Token 和正则规则落成 .l 文件

Cminus 是 C 语言的一个真子集,去掉了结构体、指针、预处理、浮点等,保留了变量声明、函数、数组、表达式、if/else、while。正因为它比 C 小得多,用作大作业的词法和语法分析对象正合适。别和前端 CSS 的 flex 布局搞混——这里的 Flex 是词法分析器生成器,它读取一个 .l 文件,里面每一行写一条正则规则和对应的动作,然后生成一个纯 C 的扫描器。

2.1 从 Cminus 语法手册里提取词法要素:关键字、运算符和标识符

写词法之前,先把 Cminus 的合法单词列出来。这不是让你背文法,而是决定 .l 文件里要写哪些规则。我会先做一张表,这样哪些规则要写、哪些不用写,一目了然:

类别内容
关键字else if int return void while
标识符字母或下划线开头,后跟字母/数字/下划线
整数十进制数字串,如 0、42
运算符+ - * / < <= > >= == != =
标点; , ( ) [ ] { }
注释/* 与 */ 之间的任意字符,可跨行

注意 Cminus 没有 &&、||、!,也没有复合赋值运算符 +=,所以你不必为它们写规则。数组写法是int a[10],下标用方括号。void只用于函数返回类型或参数列表为空的地方。这些看起来简单,但决定了大作业的覆盖面:如果老师给的 Cminus 标准里还有/* */嵌套注释,那状态机就要另写;如果没有,就别给自己加戏。

这里有一个新手很容易踩的点:关键字和标识符在正则层面长得一样。比如else是一个关键字,但它本身也匹配“标识符”那条规则。解决方法是把关键字规则写在标识符规则的前面。Flex 在同时有多个规则匹配相同长度文本时,选靠前的那条。于是else会先命中关键字规则,返回 ELSE;而els这种不在任何关键字规则里的名字,就只能走标识符规则,返回 ID。这个顺序就是词法分析的“第一性原理”。

2.2 在 Flex 里定义正则与状态:一个可直接跑通的最小 .l 文件

下面是一个能直接跑通的最小 lexer.l。它只用到了文本匹配,注释部分用%x comment状态来处理跨行注释。

%{ #include <stdlib.h> #include <string.h> #include "parser.tab.h" /* 由 bison -d 生成,包含 token 宏和 yylval 声明 */ extern int yylineno; /* 行号,交给 flex 维护 */ %} %option noyywrap %option yylineno %x comment %% "/*" { BEGIN(comment); } <comment>[^*\n]+ /* 跳过普通字符 */ <comment>"*" /* 跳过单独出现的星号 */ <comment>\n /* 注释里的换行 */ <comment>[^*]*"*/" { BEGIN(INITIAL); } "else" { return ELSE; } "if" { return IF; } "int" { return INT; } "return" { return RETURN; } "void" { return VOID; } "while" { return WHILE; } "+" { return PLUS; } "-" { return MINUS; } "*" { return TIMES; } "/" { return DIV; } "<" { return LT; } "<=" { return LTE; } ">" { return GT; } ">=" { return GTE; } "==" { return EQ; } "!=" { return NEQ; } "=" { return ASSIGN; } ";" { return SEMI; } "," { return COMMA; } "(" { return LPAREN; } ")" { return RPAREN; } "[" { return LBRACKET; } "]" { return RBRACKET; } "{" { return LBRACE; } "}" { return RBRACE; } [0-9]+ { yylval.intval = atoi(yytext); return NUM; } [A-Za-z_][A-Za-z0-9_]* { yylval.strval = strdup(yytext); return ID; } [ \t\r\n]+ ; /* 空白直接跳过 */ . { fprintf(stderr, "行 %d: 非法字符 '%s'\n", yylineno, yytext); return ERROR; } %%

这段代码的每一个关键点都值得说清楚。%option noyywrap告诉 Flex 不要生成 yywrap 的调用和引用,这样就不用额外链接-lfl。%option yylineno让 Flex 自动用全局变量yylineno记录当前行号,在遇到换行时加一,省得你每个规则里去数\n。%x comment定义了一个独占开始状态,BEGIN(comment)让扫描器进入注释状态。在注释状态里,我写了三条规则:普通字符、单独星号、换行。最后一条<comment>[^*]*"*/"负责找到注释结束符;[^*]*匹配任意非星号字符,然后"*/"收尾。把/* */处理成注释而不是/加*,是为了避免把 Cminus 的除法运算和注释搞混。

运算符规则的顺序也很重要。"<"和"<="都放在前面,但 Flex 采用最长匹配,遇到<=时整条<=会被"<="规则吃掉,不会先匹配<再单独匹配=。同理==和!=必须写在=前面。这是词法分析里所谓“最长匹配优先”:不要把短规则放在长规则前面,否则长 token 会被拦腰折断。

最后一条.是兜底规则,匹配所有未列出的字符。Cminus 不允许$、@、\之类出现,所以这里直接打印错误并返回一个 ERROR token。你不一定需要ERROR这个 token,但提前处理能让后面 Bison 的报错信息更友好。strdup返回的内存要记得在 Bison 动作里用完后释放,不然每次分析一个标识符就泄漏一次。

到这里,词法分析器已经有了。但光有.l文件编译不过,因为里面的ELSE、ID这些宏来自parser.tab.h,而它是 Bison 生成的。所以下一步必须先把语法分析器的骨架搭起来。你可以先不管语法细节,马上写一个临时的 parser.y 生成头文件,然后用下面的小工具单独验证词法。

2.3 单独测试词法:用 yylex 输出 Token 流

在没写语法分析器之前,可以先写一个极小的 test_lexer.c,把这段.l生成的lex.yy.c和临时 parser 一起编译,循环调用yylex打印 token 流:

#include <stdio.h> #include "parser.tab.h" extern int yylex(void); extern char *yytext; extern int yylineno; extern FILE *yyin; int main(int argc, char *argv[]) { if (argc > 1) { yyin = fopen(argv[1], "r"); if (!yyin) { perror("open file"); return 1; } } int token; while ((token = yylex()) != 0) { printf("line %d: token=%d text=%.*s\n", yylineno, token, (int)yytext_len(), yytext); } return 0; }

注意上面的yytext_len()是 flex 生成的一个函数,用来获取当前 token 长度,因为yytext不是以\0结尾的,直接%s可能连带打出后面缓冲区的残渣。如果你不想用这个函数,可以直接printf("text=%s", yytext),大多数场景下结果也是对的,但遇到字符串末尾紧跟缓冲区边界时可能翻车。这一步验证非常值得做:它能提前暴露 token 定义不一致、关键字被识别成 ID、注释没有跳过去等问题,等语法分析器跑起来后再调试,成本就高了。

3. 接着写语法:用 Bison 把 Cminus 文法转成 LALR(1) 语法分析器

词法负责把字符流变成 token 流,语法负责把 token 流变成一棵树(或者至少确认它符合文法)。Bison 读取 .y 文件,里面是产生式和动作,然后生成一个自底向上的 LALR(1) 分析器。Cminus 是典型的上课文法,几乎不会出现 LR 无法处理的极端情况,所以你可以放心用 Bison 默认的 LALR(1) 模式。

3.1 为 Bison 准备文法产生式:优先级与结合性的声明顺序

Cminus 的文法包括声明、函数、复合语句、表达式等。你不必在 .y 文件里一次写完所有产生式,建议先把表达式和语句搞定,再补声明。常见做法是从课程给的 Cminus 文法抄一套下来,但一定要自己重新排一遍产生式顺序,尤其是优先级声明。

在 Bison 里,优先级声明写在%%之前,先声明的优先级低,后声明的优先级高。我会这样写:

%right ASSIGN %left EQ NEQ %left LT LTE GT GTE %left PLUS MINUS %left TIMES DIV %nonassoc LOWER_THAN_ELSE %nonassoc ELSE

%right ASSIGN表示赋值是右结合的,且优先级最低。%left PLUS MINUS表示加减是左结合的,优先级比关系运算高。%left TIMES DIV再高一级。如果你有负数,可以再单独声明一个%right UMINUS,然后在factor: MINUS factor %prec UMINUS上使用,否则一元负号和二元减法会冲突。

为什么需要LOWER_THAN_ELSE和ELSE?这是经典的 if-else 悬空问题。当输入是if (a) if (b) stmt else stmt时,解析器读到内层 if 的else时,既可以归约内层 if,也可以继续读入 else 来匹配内层。Bison 默认偏向 shift,所以即使你不声明,它也会匹配最近的 if。但大作业里老师经常要求你能解释清楚,所以你在 .y 里这样显式写,并在答辩时说出“LOWER_THAN_ELSE 优先级低于 ELSE,导致遇到 ELSE 时选择 shift 而不是 reduce”,会比一句“默认就那样”有说服力得多。

3.2 在 .y 文件里挂动作:直接打印产生式还是构建 AST 节点

这里有两种路线:一种是每个产生式里用 printf 打印归约过程,验证语法;另一种是构建 AST 节点,为后续语义分析留接口。我的建议是选后者,因为大作业通常要求“分析结果可视化”或“生成中间代码”,构建 AST 是一劳永逸的事。下面给出一个带 AST 动作的 .y 骨架:

%{ #include <stdio.h> #include <stdlib.h> #include "ast.h" extern int yylex(void); extern int yylineno; void yyerror(const char *msg); %} %union { int intval; char *strval; struct ASTNode *node; } %token <intval> NUM %token <strval> ID %token ELSE IF INT RETURN VOID WHILE %token PLUS MINUS TIMES DIV LT LTE GT GTE EQ NEQ ASSIGN %token SEMI COMMA LPAREN RPAREN LBRACKET RBRACKET LBRACE RBRACE %type <node> program declaration_list declaration var_declaration func_declaration %type <node> expression simple_expression additive_expression term factor var %type <node> statement_list statement expression_statement compound_stmt if_stmt while_stmt return_stmt %right ASSIGN %left EQ NEQ %left LT LTE GT GTE %left PLUS MINUS %left TIMES DIV %nonassoc LOWER_THAN_ELSE %nonassoc ELSE %% program : declaration_list { $$ = $1; } ; declaration_list : declaration_list declaration { $$ = append_decl($1, $2); } | declaration { $$ = $1; } ; var_declaration : INT ID SEMI { $$ = make_var_decl($2, NULL); } | INT ID LBRACKET NUM RBRACKET SEMI { $$ = make_var_decl($2, $4); } ; expression : simple_expression { $$ = $1; } | var ASSIGN expression { $$ = make_assign($1, $3); } ; simple_expression : additive_expression { $$ = $1; } | simple_expression LT additive_expression { $$ = make_binary($1, LT_OP, $3); } | simple_expression EQ additive_expression { $$ = make_binary($1, EQ_OP, $3); } ; additive_expression : term { $$ = $1; } | additive_expression PLUS term { $$ = make_binary($1, PLUS_OP, $3); } | additive_expression MINUS term { $$ = make_binary($1, MINUS_OP, $3); } ; term : factor { $$ = $1; } | term TIMES factor { $$ = make_binary($1, TIMES_OP, $3); } | term DIV factor { $$ = make_binary($1, DIV_OP, $3); } ; factor : LPAREN expression RPAREN { $$ = $2; } | NUM { $$ = make_num($1); } | var { $$ = $1; } ; var : ID { $$ = make_var($1); } | ID LBRACKET expression RBRACKET { $$ = make_arr_ref($1, $3); } ; statement : expression_statement { $$ = $1; } | compound_stmt { $$ = $1; } | if_stmt { $$ = $1; } | while_stmt { $$ = $1; } | return_stmt { $$ = $1; } ; if_stmt : IF LPAREN expression RPAREN statement %prec LOWER_THAN_ELSE { $$ = make_if($3, $5, NULL); } | IF LPAREN expression RPAREN statement ELSE statement { $$ = make_if($3, $5, $7); } ; while_stmt : WHILE LPAREN expression RPAREN statement { $$ = make_while($3, $5); } ; return_stmt : RETURN SEMI { $$ = make_return(NULL); } | RETURN expression SEMI { $$ = make_return($2); } ; %%

这段代码里的make_*函数都需要你在 ast.h / ast.c 里实现。%union定义了语义值类型,%token <intval> NUM的意思是在词法分析里yylval.intval存的整数会被放进$1;%type <node> expression表示每个 expression 归约后$$是一个 AST 节点指针。

这里有一个很容易忽略的点:%token没有写在%union里的成员?不对,必须写。比如%token ELSE没有尖括号,表示这个 token 没有语义值,只是记号。在 flex 里你return ELSE;之前不需要赋yylval。而NUM、ID有语义值,所以必须显式带上<intval>或<strval>。如果漏了,Flex 里给yylval赋值时编译器会报yylval类型不完整或成员不存在。

3.3 编译顺序:bison -d 必须在 flex 之前跑

这一步非常关键,很多人的大作业翻车就在这。Bison 的-d选项会生成parser.tab.h,里面定义了ELSE、ID这些 token 的宏编号,还有YYSTYPE联合体。Flex 的 lexer.l 要 include 这个头文件,才能让return ELSE;里的 ELSE 有定义,以及yylval.intval能通过编译。

正确的编译顺序是:

bison -d parser.y flex lexer.l gcc -o cminus parser.tab.c lex.yy.c ast.c main.c

如果你先跑 flex,再改 parser.y 里新增了一个 token,然后只重跑 bison,那么 lex.yy.c 里旧的 token 宏编号可能不变,但parser.tab.h里编号变了,最终链接出来运行时 token 错位,整个分析器行为诡异。所以我在 Makefile 里会强制依赖:

parser.tab.h parser.tab.c: parser.y bison -d parser.y lex.yy.c: lexer.l parser.tab.h flex lexer.l cminus: parser.tab.c lex.yy.c ast.c main.c gcc -o $@ $^

main.c里要负责打开文件、提供yyerror实现,否则链接时一堆 undefined reference。这也是很多新手拿到模板后直接gcc lex.yy.c parser.tab.c报错的原因:不是 Bison 或 Flex 的问题,是缺少yyerror、main这些 C 层面的支撑函数。

4. 前后端拼起来:主程序怎么协调 yylex、yyparse 和符号表

有了词法和语法两段生成代码,接下来是缝合成一个可执行的程序。很多大作业的源码包之所以看着乱,是因为大家把 flex、bison、手写 C 文件混在一起,不知道各自该暴露什么接口。这一章把接口边界说清楚。

4.1 词法单元传递:让 Bison 通过 yylval 拿到 Token 的属性值

在 Flex 中,每一条规则里的动作决定了这个 token 除了“它是谁”之外,还携带什么属性。比如 NUM 需要携带数值,ID 需要携带名字。做法是在return之前给全局变量yylval的对应成员赋值:

[0-9]+ { yylval.intval = atoi(yytext); return NUM; } [A-Za-z_][A-Za-z0-9_]* { yylval.strval = strdup(yytext); return ID; }

yylval的类型是YYSTYPE,也就是你在 .y 里写的%union。如果 token 声明为%token <intval> NUM,那么 Bison 在动作里引用$1时会自动取yylval.intval对应的成员。如果声明不一致,例如 .y 里是%token <strval> NUM,flex 这边赋的是intval,那么你在 Bison 动作里打印$1时拿到的是字符串指针,轻则打印乱码,重则段错误。这种问题编译时不一定报错,因为 union 大小一样,类型标签无法在 C 层面检查。所以一定要保持 flex 和 bison 两边对同一个 token 的“成员名”一致。

主程序最简单的样子是:

#include <stdio.h> #include <stdlib.h> #include "parser.tab.h" extern FILE *yyin; extern int yyparse(void); extern void yyrestart(FILE *f); int main(int argc, char *argv[]) { if (argc > 1) { yyin = fopen(argv[1], "r"); if (!yyin) { perror("open file"); return 1; } yyrestart(yyin); } int result = yyparse(); if (result == 0) { printf("syntax OK\n"); } else { fprintf(stderr, "syntax error\n"); } return result; }

yyrestart(yyin)是 flex 提供的函数,用来重置内部缓冲区。如果yyin指向了stdin,可以不调用;但如果多次解析不同文件,必须yyrestart,否则缓冲区里残留上一次解析的字符。另外yyparse的返回值是 0 表示成功,1 表示语法错误,2 表示内存不足,这是 Bison 的固定约定。

4.2 错误恢复与调试开关:yydebug、error token 和日志输出

调试语法分析器最好的方式是打开 Bison 的调试输出。在 .y 文件里加一行%debug,或者在 main 里手动把全局变量yydebug设为 1:

extern int yydebug; int main(int argc, char *argv[]) { #ifdef YYDEBUG yydebug = 1; #endif ... }

启动后,Bison 会把 shift、reduce、goto 的每一步状态变化打印到 stdout。你会看到类似Reading a token、Next token is token IF、Shifting token这样的输出。配合 flex 侧的yylineno,基本可以定位到某一个具体 token 导致的分析失败。

但注意:大面积调试输出会让小文件也刷屏,所以我一般只在-d参数里临时开,或者用环境变量控制。更可控的做法是在yyerror里打印当前 token 的文本和行号:

void yyerror(const char *msg) { extern int yylineno; extern char *yytext; fprintf(stderr, "解析错误: 行 %d, 最近 token '%s', 原因 %s\n", yylineno, yytext, msg); }

yytext在语法分析阶段指向的是当前 token 的文本,但注意它是 flex 的全局缓冲,可能在调用yyerror时已经被覆盖,所以有些版本下打印出来是空或乱码。更稳妥的是用yylval.strval里保存的值,或者干脆只报行号和错误信息。大作业项目里,错误恢复也常被问到。所谓错误恢复,就是在 .y 里加error符号:

statement : expression_statement | compound_stmt | if_stmt | while_stmt | return_stmt | error SEMI { yyerror("语句错误,尝试跳过到分号"); yyerrok; } ;

当分析器遇到不合法的 statement 开头时,会 pop 掉栈里的状态,直到找到能接受error的状态,然后读入error并继续。yyerrok告诉 Bison 错误已经恢复,不再把这个错误状态一直传播下去。

这里有一个度的问题:我偏好只在 statement 级和 declaration 级做恢复,不在 expression 里做。因为在表达式里做 error,一个括号没闭合,可能触发几十条错误恢复,后面的报错信息完全没有参考价值。老师看输出时,更愿意看到一个“语法错误在第 12 行列”,而不是满屏的向量错误。

5. 联调避坑:Flex 和 Bison 配合时最容易翻车的 5 个现场

这一章是给到实际运行阶段的血泪经验。以下每个问题都是大作业里出现频率最高的“黑匣子”现象,按现象、原因、解决三个层次写。

5.1 坑一:yylval 类型对不上,编译能过但运行段错误

现象:写了%union { int intval; char *strval; struct ASTNode *node; },也声明了%token <intval> NUM,在 flex 里赋了yylval.intval = atoi(yytext)。结果运行时,遇到数字构成的表达式就直接 Segmentation fault。

原因:最常见的是parser.tab.h没有被重新生成。你之前编译过一次,后来改了 .y 文件里的%union成员顺序或类型,但 flex 的 lex.yy.c 还是在用旧头文件里的YYSTYPE,两者内存布局一致但成员名不同,加上yylval是个 union,flex 赋值时按旧偏移写,Bison 读时按新偏移读,自然错位。

解决:删掉所有.o和生成文件,严格按 “bison -d → flex → gcc” 的顺序重新编译。如果还出错,打开parser.tab.h,看YYSTYPE里成员名是否和你 flex 里用的一致。另外,在 flex 文件里最好#include "parser.tab.h"放在所有%{%}内容的最前面,避免被其他宏干扰。

5.2 坑二:报错行号不对,永远是 1 或者是上一行

现象:词法分析器已经能识别 token,语法分析报错时yylineno打在 1 行,或者每次报错都比实际行号小一。

原因:你用了%option yylineno,但自己又在规则里写了yylineno++;。Flex 的%option yylineno会在每次匹配到包含换行的文本后自动增加,重复手写会导致行号翻倍。另一种情况是注释或字符串跨行时没有匹配换行符,导致行号不推进。

解决:删掉手写的行号自增逻辑,把%option yylineno打开。同时检查注释状态里确实有<comment>\n规则匹配换行。注意,如果你用[ \t\n]+来跳过空白,这条规则会吃掉\n,flex 会按匹配文本里的换行数自动给yylineno加一,所以空白规则里不需要再写动作。测试时单独写两行代码,看 token 打印是否按行号递增。

5.3 坑三:改了 token 定义,程序行为还是旧的

现象:你在 parser.y 里新增了一个%token FOR,然后在 flex 里写了"for" { return FOR; },但编译后,代码里输入 for 却报 unknown character。

原因:flex lexer.l生成的 lex.yy.c 用到了从parser.tab.h里读到的 FOR 宏编号。如果你只改了 .y,没有重新执行bison -d,那么 FOR 在头文件里不存在,flex 编译时直接报错;更隐蔽的是,你的 Makefile 里写错了依赖顺序,导致 gcc 复用了旧的目标文件。

解决:这种情况下,最有效的办法是执行make clean再重新编译。为了省事,在 Makefile 里把lex.yy.c的依赖设为lexer.l parser.tab.h,把parser.tab.h的依赖设为parser.y,这样只要一边变,另一边就必须重新生成。这个坑的本质不是 Flex 和 Bison 本身的问题,而是构建系统的时序问题,但几乎所有做过大作业的人都会踩一次。

5.4 坑四:Bison 报 shift/reduce 或 reduce/reduce 冲突

现象:bison -d parser.y时终端输出警告:conflicts: 1 shift/reduce。你不太在意,继续运行,结果很多合法程序被拒绝,或者错误恢复后行为很奇怪。

原因:冲突说明文法里有二义性。最常见来源是 if-else 悬空和表达式优先级没有声明;两个可空产生式同时出现时还会产生 reduce/reduce。Bison 默认会倾向于 shift,这在 if-else 场景下是对的,但如果你没有显式声明运算符优先级,比如expression: expression PLUS term和expression: expression TIMES term,当读到+时不知道是该归约上一个加法还是继续读乘法,就会产生 shift/reduce 冲突,解析器可能按错误的方向走。

解决:用bison -v生成.output状态文件,打开后直接搜 “state XX conflicts”。在表达式规则上按从低到高的顺序声明%left和%right。if-else 用%nonassoc LOWER_THAN_ELSE和%nonassoc ELSE两档来强制 shift。如果是 reduce/reduce,通常要改写文法,比如把两个可空非终结符合并,或引入一个marker非终结符来创造上下文。不要盲目增加%left,它只能解决 shift/reduce,不能解决所有 reduce/reduce。

5.5 坑五:Windows 下源文件带 UTF-8 BOM,第一行报非法字符

现象:在 Windows 上用 VS Code 写 lexer.l / parser.y,保存时默认可能带 UTF-8 BOM。运行程序后,第一个字符还没读就被报非法字符,而且报错位置在文件开头,换行符怎么处理都不行。

原因:Flex 把 UTF-8 BOM 当作普通字节流处理。BOM 的 EF BB BF 三个字节中,第一个字节 EF 无法被任何规则匹配,于是触发兜底.规则,报非法字符。这种情况不是规则写错,而是编辑器没做字节级干净处理。

解决:用编辑器右下角把编码改为UTF-8 with no BOM,或者重新保存为GBK也行。如果已经生成lex.yy.c,记得同时删除重新生成。还有一个更稳的方法是在 flex 规则库开头加一行\xEF\xBB\xBF { /* skip BOM */ },但 Flex 的正则字面量不一定支持\x转义,不同版本行为不同,不如直接从源头解决。这个问题看着小,却能把人卡住半小时,属于典型的“环境坑”。

6. 进阶:给 Cminus 分析器加一个 --dump-tokens 开关,答辩时直接验证

词法和语法分析器都跑通后,别急着交。我建议你给主程序加两个命令行参数:--dump-tokens用来单独输出 token 流,--debug用来打开 Bison 调试输出。这样老师验证时,可以先看到词法结果,再看到语法树或者归约过程,而不是只给一个黑盒的 pass/fail。

你可以这样组织 main.c:

#include <stdio.h> #include <string.h> #include "parser.tab.h" extern FILE *yyin; extern int yylex(void); extern int yyparse(void); extern int yydebug; int main(int argc, char *argv[]) { const char *filename = NULL; int dump_tokens = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--dump-tokens") == 0) dump_tokens = 1; else if (strcmp(argv[i], "--debug") == 0) yydebug = 1; else filename = argv[i]; } if (filename) { yyin = fopen(filename, "r"); if (!yyin) { perror(filename); return 1; } } if (dump_tokens) { int tok; while ((tok = yylex()) != 0) { printf("%d:%s\n", tok, ??); } return 0; } return yyparse(); }

这里有个值得注意的小技巧:不要把--dump-tokens和语法分析混在一起。词法错误和语法错误混着报时,很难判断是 Flex 规则写错还是 Bison 文法写错。先 dump token,确认字符流切分正确,再跑语法分析,定位成本能少一半。当年我自己第一次做类似的作业,就是直接调yyparse,结果一个变量名里混了下划线,词法把它读成两个 ID,Bison 一路报错,我盯着文法看了半天才发现是正则写漏了下划线。后来养成了先 dump token 再 parse 的习惯。

验证时,我还会拿几个边界用例测:int a[10];这种数组声明,if (a) b = 1; else b = 2;这种悬空 else,以及a[2+3] = 4;这种含表达式下标的赋值。如果这三个用例都能在--dump-tokens里拿到预期的 token 序列,在yyparse里零错误通过,那大作业的语法分析部分基本稳了。希望这个思路能帮到你,少走几天弯路。

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

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

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

立即咨询