ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

编译原理实验报告1:词法分析器设计与状态转换图实战

编译原理实验报告1:词法分析器设计与状态转换图实战 简介西南科技大学编译原理实验报告主题为设计词法分析程序适合计算机专业学生、备考者及需要完成编译原理实验的学习者参考。报告完整呈现实验目的、实验设计、实验过程与程序实现涵盖正则表达式描述词法规则、非确定有限自动机构造与合并、确定化并化简为最小确定有限自动机等关键环节同时给出标识符、保留字、无符号整数、分界符、运算符、注释符等单词分类及输出方案并附有Python词法分析程序的状态转移框架。报告中的状态转移表与字符分类函数示例可帮助读者快速上手词法分析器编写用于课程设计、复习备考或比对自身实现。资源为单个doc文档压缩包约444KB内容排版完整、可直接查看。目前已有477人学习下载是理解词法分析原理、撰写实验报告的有益资料。1. 编译原理实验报告1先搞清楚这门实验到底在训练什么西南科技大学编译原理实验报告1通常是整个编译原理课程里第一次真正动手的节点。很多同学以为这是“写一份报告交差”实际上它对应的是从“理解词法规则”到“能写出一个可运行的词法分析程序”的跨越。报告本身倒是次要的程序能不能把一段源代码切成正确的token流才是实验的核心考核点。第一次实验的典型形态是给定一门小型语言的词法规则关键字、标识符、数字、运算符、界符要求用C/C/Java/Python实现一个扫描器支持从文件读入源码、输出token序列和符号表最后把设计过程、状态转换图、核心代码、测试结果整理成实验报告。适合的人群很明确——正在修编译原理学分、同时想真正把“正则表达式到自动机”这条路走通的学生。前面理论课听懂了不等于能写出来这篇就把从设计到成稿的完整路径拆开讲。2. 搭环境与定报告框架先让整个流程有一个能跑的骨架2.1 语言与工具选型别让环境问题吃掉你三天时间编译原理实验1的语言选择直接影响后续所有步骤。常见做法是C语言、Java、Python三选一但选型不能只凭“哪个熟用哪个”要看实验报告要求的深度和后续实验的衔接关系。如果后续实验是“基于Flex/Bison的语法分析”用C/C写词法更贴合后续直接对接Bison生成的parser。如果后续实验是“递归下降语法分析”用Java或Python写起来更快字符串处理也省力。如果只是为了这次报告Python是最短路径但老师如果要求画状态转换图并体现“状态编码”C语言的结构体switch反而更好讲清楚。我一般建议西南科大的同学选C语言理由是课程考核里“状态转换图与代码的对应关系”是一个明确的评分点C语言的switch-case和状态枚举能让这份对应关系一目了然。工具链只需要一个gcc和一个文本编辑器不需要额外环境。// token.h #ifndef TOKEN_H #define TOKEN_H typedef enum { T_ID, // 标识符 T_NUM, // 整数常亮 T_KEY, // 关键字 T_OP, // 运算符 T_DELIM, // 界符 T_EOF // 文件结束 } TokenType; typedef struct { TokenType type; // 种别编码 char lexeme[64]; // 单词文本 int line; // 行号 } Token; #endif这段代码定义了token的数据结构种别编码、单词文本、行号三个字段就是后续所有处理的最小公共接口。为什么要单独建头文件而不是写在一个源文件里因为实验报告里需要展示“模块划分”——这个头文件就是词法分析器与外部程序的约定后续语法分析实验直接复用。2.2 程序结构设计词法、符号表、驱动三部分各管各的事完整的词法分析程序至少包含三个模块扫描器主逻辑、符号表管理、测试驱动入口。很多第一次做实验的同学把全部代码塞进一个main函数报告里“模块划分”一栏就无话可说。// lexer.c #include ctype.h #include stdio.h #include string.h #include token.h static FILE *src_file; static int current_line 1; Token get_next_token(void); static int peek_char(void) { int c fgetc(src_file); if (c \n) current_line; return c; } static void unpeek_char(int c) { if (c \n) current_line--; ungetc(c, src_file); }这里把文件读取封装成了peek_char和unpeek_char两个函数是词法分析器的骨架。为什么要这样设计因为扫描器必须能“往前多看一个字符”再决定是否回退——比如读到了需要再看下一个字符是不是如果是就组成不是就把读到的字符还回去。如果你直接用getchar裸读后面处理回退会很狼狈。这个“预读回退”机制是词法分析的通用套路报告里写状态转换图之前建议先用这个骨架把程序跑通。2.3 实验报告框架三个时间点分别写什么别留到最后一天实验报告不是一次性写完的合理的做法是分三个时间节点填充实验前写设计部分状态转换图、token种别编码表、算法思路。这三样东西设计阶段确定了代码就是照图施工。实验中写实现日志遇到什么问题、卡在哪里、怎么解决的。这一步很多同学不做但它是报告里“问题分析”部分的真实素材。实验后写测试与结论用哪些测试用例、输出是否符合预期、还有哪些边界没覆盖。这种写法的好处是报告的每一部分都有实际依据不会出现“实验步骤全是套话”的情况。一般模板结构是实验目的、实验内容、实验设计重点、实现过程含核心代码、测试与分析、实验总结。其中“实验设计”和“测试与分析”是打分重区。3. 词法分析从设计到实现状态转换图先于代码报告才有得写3.1 状态转换图怎么画一个数字识别器的完整推导过程词法分析实验最容易翻车的环节是代码写完了状态转换图画不出来。因为代码逻辑是东拼西凑的根本没有经过“先设计状态机”这一步。一个合格的词法分析器设计应该先从状态转换图出发。以“整型常量识别”为例状态设计如下状态0初态读到数字0-9进入状态1其他字符走其他分支。状态1继续读数字仍在状态1读到非数字字符则回退返回状态1为终态。就这么简单的规则当它变成报告里的图需要用标准符号圆圈代表状态单圈是中间态双圈是终态弧线上标注触发字符。状态0到状态1的弧线上写digit状态1指向自身的弧线上也写digit。画完图再写代码// 数字识别的状态机实现 static Token parse_number(int first_digit) { Token tok; char buf[32]; int len 0; buf[len] (char)first_digit; int c; while (isdigit(c peek_char())) { if (len 31) buf[len] (char)c; } if (!feof(src_file)) unpeek_char(c); // 读到非数字字符回退 buf[len] \0; tok.type T_NUM; strcpy(tok.lexeme, buf); tok.line current_line; return tok; }注意那段unpeek_char(c)——读到非数字字符必须回退否则123abc会被错误地切分成123和abc之间的空格丢失问题。这个回退机制就是状态转换图里“从状态1回到状态0时读到的非digit字符不消费”的代码表达。报告里写“代码与状态图的对应关系”时直接引用这两处就行。3.2 种别编码表报告里的表比代码里的枚举更重要Token的种别编码是实验报告里必须出现的表格。按实验指导书的常见约定编码规则如下单词类型种别编码具体例子关键字1-10if(1) else(2) while(3) for(4) int(5) return(6) void(7) break(8) continue(9) main(10)标识符11用户自定义的变量名、函数名整型常量12123、456运算符13-29(13) -(14) *(15) /(16) (17) (18) !(19) (20) (21) (22) (23)界符30-36;(30) ,(31) ((32) )(33) {(34) }(35) (36)关键字的设计有一个常见的坑先识别出标识符再查关键字表。也就是说识别逻辑是“凡是字母开头的字母数字串都按标识符处理然后查预定义关键字哈希表命中就改类型为T_KEY”。这样比“每次读字符都判断是否匹配某个关键字”要高效得多报告里要写清楚这个先后顺序。// 标识符或关键字的统一处理 Token parse_id_or_keyword(int first_char) { Token tok; char buf[64]; int len 0; buf[len] (char)first_char; int c; while (isalnum(c peek_char()) || c _) { if (len 63) buf[len] (char)c; } if (!feof(src_file)) unpeek_char(c); buf[len] \0; tok.line current_line; // 查关键字表 const char *keywords[] {if, else, while, for, int, return, void, break, continue, main}; int is_keyword 0; for (int i 0; i 10; i) { if (strcmp(buf, keywords[i]) 0) { tok.type T_KEY; tok.lexeme[0] (char)(0 i 1); // 种别编码直接映射 strcpy(tok.lexeme, buf); is_keyword 1; break; } } if (!is_keyword) { tok.type T_ID; strcpy(tok.lexeme, buf); } return tok; }这里用了一个直接映射的小技巧种别编码1-10正好对应关键字数组下标1省去二次查找。但实际作业里不建议用这种隐式映射因为可读性差而且关键字表一变就容易出bug。更好的做法是用一个结构体数组存关键字和编码的配对关系。3.3 完整的token输出与错误恢复报告里要展示的“拒绝”与“跳过”词法实验中有一个隐藏考点——遇到非法字符怎么处理。比如源码里出现、$这类不在词法规则里的字符扫描器不能直接崩溃应该报告错误并跳过。int error_count 0; Token get_next_token(void) { int c; // 跳过空白字符 while ((c peek_char()) ! EOF) { if (isspace(c)) continue; break; } if (c EOF) { Token tok {T_EOF, , current_line}; return tok; } // 标识符和关键字 if (isalpha(c) || c _) { return parse_id_or_keyword(c); } // 数字 if (isdigit(c)) { return parse_number(c); } // 运算符和界符 switch (c) { case : { Token t {T_OP, , current_line}; return t; } case : { int next peek_char(); if (next ) { Token t {T_OP, , current_line}; return t; } else { unpeek_char(next); Token t {T_OP, , current_line}; return t; } } // ... 其他运算符 default: error_count; fprintf(stderr, 第%d行: 非法字符 %c\n, current_line, c); return get_next_token(); // 跳过非法字符继续扫描 } }这个递归调用get_next_token()跳过非法字符的处理方式看起来简单但是一个很实用的设计——词法分析阶段不应该把错误判断为“整个编译失败”而应该记录错误数量、跳过非法字符、继续分析。这样一次能报告尽可能多的词法错误。报告的“错误处理”小节里写这段能明显拉开得分差距。4. 主循环与测试用例从能跑通到能展示正确性4.1 带符号表的驱动主程序让输出格式满足实验要求实验报告最常规的验收方式是一张输出表序号、单词文本、种别编码、行号。因此主循环要把token逐个输出并把标识符填入符号表。// main.c #include stdio.h #include string.h #include token.h #define SYMBOL_TABLE_SIZE 128 typedef struct { char name[64]; TokenType type; } SymbolEntry; SymbolEntry sym_table[SYMBOL_TABLE_SIZE]; int sym_count 0; int find_symbol(const char *name) { for (int i 0; i sym_count; i) { if (strcmp(sym_table[i].name, name) 0) return i; } return -1; } int insert_symbol(const char *name, TokenType type) { int idx find_symbol(name); if (idx ! -1) return idx; if (sym_count SYMBOL_TABLE_SIZE) { strcpy(sym_table[sym_count].name, name); sym_table[sym_count].type type; return sym_count; } return -1; } int main(int argc, char *argv[]) { if (argc 2) { fprintf(stderr, 用法: %s 源文件\n, argv[0]); return 1; } src_file fopen(argv[1], r); if (!src_file) { perror(打开文件失败); return 1; } Token tok; int seq 0; while (1) { tok get_next_token(); if (tok.type T_EOF) break; printf(%d\t%s\t%d\t第%d行\n, seq, tok.lexeme, tok.type, tok.line); if (tok.type T_ID) { int idx insert_symbol(tok.lexeme, T_ID); printf( - 写入符号表位置 %d\n, idx); } } printf(\n词法错误数: %d\n, error_count); printf(符号表条目数: %d\n, sym_count); fclose(src_file); return 0; }主程序把词法分析、符号表管理、错误统计都串起来了。报告里测试部分直接贴这个程序对示例代码的输出。编译器命令也值得写进报告体现可复现性gcc -Wall -o lexer main.c lexer.c ./lexer test.c-Wall开启所有警告写实验时建议强制开启——如果代码有未使用的变量或者危险的类型转换编译器会提示报告中能少一个“程序运行结果与预期不符”的回头路。4.2 测试用例设计覆盖正常路径、边界路径、错误路径测试用例是实验报告里“实验数据与分析”的打分重点。很多同学只拿一段最简单的int main(){return 0;}测一遍就交老师一问“你的词法分析器能处理哪些边界情况”就答不上来。三个层次的测试用例第一层正常路径int main() { int sum 0; for (int i 1; i 10; i) { sum sum i; } return sum; }这个用例覆盖关键字、普通标识符、数字、运算符、界符、赋值运算和比较运算。第二层边界路径int a123 12; float _tmp 3.14; if (a123 12 _tmp 0) { }这个用例里_tmp是合法的标识符下划线开头3.14在有的词法规则里不是合法数字不含小数点规则的话会切分成3和.和14是否作为单独的运算符也是需要查询实验指导书的。这些边界是体现水平的地方。第三层错误路径int 1abc 5; int x 10;1abc是典型的词法错误——按规则以数字开头的字母数字串应该被切分为数字1和标识符abc而不是报错。究竟是不报错直接切分还是报“非法标识符”取决于实验要求里的词法定义。报告里写清楚“本实验处理什么情况、为什么这样处理”即可这就是你的设计决策。4.3 测试用例对比表为什么你的输出和参考答案不同这里要给一个实验报告里常用的自检表体现出对算法边界的掌握输入片段预期输出token序列常见错误结果原因分析ifx1ifx(标识符)(运算符)1(数字)if(关键字)x(标识符) ...关键字匹配必须基于完整单词不能用前缀匹配1abc1(数字)abc(标识符)报错“非法字符”或整体拒绝扫描到a时数字状态结束回退后按标识符规则继续ababab是双字符运算符必须预读两个字符再决定/* comment */不产生token跳过把/和*分别输出注释处理要在扫描器层消化掉否则会把注释内容误分析这张表是报告里的“含金量”所在——它说明你不仅跑完了程序还理解了自己写的每一段判断是干什么的。5. 实验报告避坑指南5个高频翻车点现象、原因、解决一次说清5.1 关键字的识别顺序写反ifx被拆成if和x现象输入ifx1程序输出的是关键字if和标识符x两个token但标准词法规则里ifx应该是一个完整的标识符。原因代码把“先判断是否是关键字前缀”放在“先识别完整标识符”之前用到了前缀匹配而不是整词匹配。解决统一采用“按标识符规则读完整个字母数字串再查关键字表”的顺序。关键字表里存的是完整单词不存在前缀匹配问题。5.2 文件末尾的EOF触发无限循环现象程序输出大量重复的最后一个token甚至陷入死循环。原因处理EOF时先做了unpeek_char(c)把EOF又塞回缓冲下次读取永远读出来还是EOF循环出不去。解决在调用unpeek_char之前判断c ! EOF。这是一个纯代码层面的边界问题和编译原理理论无关但实验里最常见。if (c ! EOF) unpeek_char(c);5.3 测试代码里用了3.14程序直接崩了现象输入浮点数3.14词法分析器在.处不知道走哪个分支报错退出。原因实验的词法规则没定义小数点运算符也没定义浮点数.字符既不是运算符也不是界符同一个字符没地方认领就出错。解决先查实验指导书的token表确认.是否属于界符。如果属于按界符输出如果不属于则应该在错误处理分支里报告“非法字符”并跳过而不是崩溃。结果正确与否反而不重要重要的是报告里写清楚当时的设计判断。5.4 报告里的状态转换图和代码逻辑不一致被老师一眼看穿现象报告里画的状态转换图说数字只有整数代码里却支持了十六进制0x前缀的识别或者是代码里对的处理状态图里没画。原因写报告时先忙代码、后补图导致图是“照着别人的模板改的”跟自己的实际实现对不上。解决反着来——先把状态图确定下来再写代码。代码里任何一个分支都必须能在图上找到对应的弧线。图与代码的对应关系本身就是评分点不必“美化”到超越实际实现。5.5 只贴了核心代码但没贴编译命令老师复现不出来现象报告贴在最后的核心代码能看但不知道用什么命令编译、用什么命令测试评分老师无法验证结果。原因默认环境是IDE或者在线编译器没有写清楚命令行工具链。解决在报告的“编译环境”或“运行方法”里写两行bash命令编译命令、运行命令附带输入样例文件内容。这个在实验报告里一般是单独一个小节别放在代码注释里。6. 从“能交”到“被当范本”两个验证和一个归档习惯值得做6.1 用一个二十行以上的“真实感”程序做最后验收实验1的测试样例如果只是三五行的玩具代码边界覆盖必然不够。我建议在报告的测试部分放一个二十行以上的示例程序贴近真实C语言风格——包含多层花括号嵌套、字符串注释、多行代码、连续赋值等特征。这样的测试用例跑通后输出能横跨多页报告显得充实也更能暴露隐藏bug。一个值得放上去的验收样例/* 一个简单的整数累加程序 */ int main() { int i 0; int result 0; while (i 100) { if (i % 2 0) { result result i; } i i 1; } return result; }注释要检验跳过逻辑多行代码要检验行号递增逻辑%和和分别对应不同运算符分支。最后看输出行号是否与实际行号对齐——这个细节也能写进报告。6.2 用git管理实验代码commit信息就是过程日记实验过程最怕什么改了半天的代码改完反而坏了却没有后悔药。这是git最实在的用法。实验1的代码量不大但足够用git管理迭代git init git add token.h lexer.c main.c git commit -m 初版能识别关键字、标识符、数字和基本运算符 # 后续每次修复bug单独提交 git commit -m 修复EOF导致无限循环的问题 git commit -m 增加注释跳过功能commit信息写到这个粒度实验报告的“实现过程”部分几乎不用另外想素材——直接复制commit记录里的关键节点就能变成一份可信的开发日志。这比结束后靠记忆补写的过程真实得多。6.3 一个值得保留的习惯把词法规则表和种别编码放在代码文件头部注释里这是本人踩过最深的一个坑——实验过了两个月到语法分析实验要用词法分析器的输出才想起来当时的token编码表找不到了。从那以后任何实验代码的源文件头部必定写清楚这份规则表/* * 词法规则表 * 关键字种别: 1-10 (if, else, while, for, int, return, void, break, continue, main) * 标识符种别: 11 * 数字种别: 12 * 运算符种别: 13-29 * 界符种别: 30-37 * 非法字符: 跳过并报告错误 */这份注释在后来的语法分析实验里救了大命——中间代码生成阶段需要直接复用种别编码不需要重新看代码猜当时的枚举值。编译原理课程通常是连续几个实验层层递进的实验1的代码不只是交一份报告它还是实验2、实验3的输入。这种前后衔接怎么强调都不过分。西南科大的编译原理实验报告1说到底是一道“能否把理论课的状态转换图变成可运行的扫描器”的开放题。实验报告是记录这次转换过程的最佳载体。如果读完这篇你还有犹豫建议从状态转换图开始画起图出来了代码和报告各完成了一半。希望这篇能帮你有惊无险地过关。本文还有配套的精品资源点击获取
返回列表