简介面向编译原理课程实验的《设计词法分析程序》实验报告来自西南科技大学适合正在学习编译器前端、需要完成词法分析实验的本科生参考。该文档完整记录了TEST语言词法分析程序的设计过程包括正则表达式描述词法规则、由正则表达式构造NFA、合并确定化与最小化得到DFA以及基于Python的DFA状态机核心实现框架并附有状态转移表和单词输出方案。可帮助读者理解标识符、关键字、无符号整数、运算符、分界符、注释符等类别的判定方式掌握词法分析程序的整体流程。整个资源为1个doc文件大小约444KB内容结构清晰、实验步骤完整适合作为实验报告撰写、课程设计或复习备考的参考。该资源包已有477人浏览学习具有一定参考价值。1. 编译原理实验报告1在做什么先分清你要写的是词法分析还是语法分析凌晨一点学弟发来一张截图西南科技大学编译原理实验报告1标题下面一片空白光标在闪烁。这种状态我太熟了——题面上只写着「词法分析实验」五个字教材翻到第二章满页的正规式和状态转移矩阵就是不知道报告里该放什么。编译原理实验报告1在所有学校的流程里都逃不开同一个起点词法分析器也就是把源代码拆成一个个有意义的 token。实验报告的分数从来不取决于你写了多少页、排版多漂亮而取决于老师照着你的报告能不能把你的代码复现出来、跑出同样的结果。这篇笔记写给两类人。一是刚拿到题目、不知道第一步往哪走的同学——照着下面的结构一个晚上能把代码和报告都落地二是代码写完了但心里没底的同学——报告的避坑章节和验证技巧能帮你少交一次返工稿。下面所有的讨论围绕「报告1 词法分析器」展开如果你的课程把综合分析放到实验3以后这份前置准备完全通用。2. 词法分析器为什么绕不开状态机从正规式到 DFA 的一步到位2.1 实验报告1的边界输入是 C 语言子集输出是 token 序列实验报告1最容易被忽略的是「边界」两个字。老师不会让你识别完整的 C 语言——那需要几千行代码和完整的 Unicode 处理。常见的题目范围是一组 C 语言子集int、void、if、else、while、return 这几个关键字标识符十进制整数四则运算符赋值号与等号括号和分号。把这个边界用正规式列出来就是教材第二章那张表的标准内容。我一般建议学生在动手写代码之前先用纸把输入和输出各写一个例子。比如输入int main() { a 1 2 * 3; }期望输出是一串有序的 token 序列line1: KEYWORD(int) line1: IDENTIFIER(main) line1: LPAREN( line1: RPAREN) line1: LBRACE{ line2: IDENTIFIER(a) line2: ASSIGN() line2: NUMBER(1) line2: OP_PLUS() line2: NUMBER(2) line2: OP_MUL(*) line2: NUMBER(3) line2: SEMI(;) line3: RBRACE}把这个输入输出例放在报告的最前面整份报告的逻辑就立住了。代码要解决的技术问题只有一个给定一串字符怎么确定「main」是一个标识符而不是三个字母「12」为什么必须拆成数字、加号、数字三个 token而不是把「12」整个吞掉。这个问题用循环加 if 也能硬写但一旦 token 种类超过十个硬写的分支会让报告里那张图完全画不出来。2.2 两条实现路线硬编码串行判断与状态机我建议你选哪条把「识别 token」这件事落成代码常见的有两种路线。路线 A 是逐字符 if-else。每读到一个字符就判断是字母进标识符分支是数字进数字分支是运算符进运算符分支。这种写法在实验报告的初稿里非常常见写到第二十个 if 的时候你会发现新加一个 token 类型要改三个地方而且报告里的 DFA 图画好了代码却对不上。路线 B 是状态机。把词法分析器看成一台只认字符的机器机器有一个当前状态每读入一个字符就产生一次状态转移。这就是教材第二章那台 DFA 的地道落地。真正的编译原理课程里NFA 转 DFA、DFA 最小化这些算法实验报告1并不要求实现——你不需要把子集构造算法写一遍只需要手工构造出一台 DFA然后用代码把状态转移表描述出来。这一步我把话说透实验报告1的状态机代码本质上是把一张二维表写成了程序状态是行输入字符类别是列表里填的是下一个状态。这两条路线的差别在代码量和可解释性上非常明显。状态机版本的核心是一个 switch(state)每个状态一个 case代码行为和报告里的图一一对应。助教追问「输入 / 号之后进哪个状态」你指着报告里的图就能答。硬编码版本则是一个黑匣子答辩时自己都可能忘了当初为什么这么写。2.3 手工构造状态机的三个关键决策点最大匹配、关键字时机与词素存储决策点一最大匹配。识别标识符时只要当前字符是字母、数字或下划线就继续读直到碰到不属于标识符的字符为止。识别数字同理一直读连续的数字。关键是识别结束后要回头看一眼比如数字后面紧跟字母这属于非法词素不能把「123abc」拆成「123」和「abc」要单独报错。决策点二关键字的识别时机。很多初版代码在读到字母「i」时就尝试匹配「int」结果输入「ink」被拆成了关键字加尾巴。正确顺序是先把整个标识符词素读完整再去查关键字表查到了就是关键字查不到就是标识符。这个顺序反了关键字和标识符永远分不清这是实验报告1最典型的翻车点。决策点三词素的存储。每识别一个 token要有一个小缓冲区把组成它的原始字符存下来token 类型、词素原文、行号三个字段一起放进输出结构。缓冲区每次识别完要清空行号在跳过换行符时自增。这三个字段看着简单却是后续所有错误报告的地基——报错信息里没有行号老师根本没法定位你程序的输入是哪一行。3. 写一个能交差的词法分析器C 语言实现的数据结构、状态转移与报错3.1 先定数据结构Token 类型枚举、关键字表与缓冲区上限代码的第一步是把 token 的类型定下来。实验报告1用 C 语言写最直接因为教材的伪代码本身就是 C 风格助教审阅成本低。下面这个头文件片段定义了一组完整的数据结构#define MAX_LEXEME 64 // 单个词素最大长度 #define MAX_TOKENS 4096 // 最多输出的 token 数 typedef enum { TK_KEYWORD, // 关键字int/void/if/else/while/return TK_IDENT, // 标识符 TK_NUM, // 十进制整数 TK_PLUS, // TK_MINUS, // - TK_STAR, // * TK_SLASH, // / TK_ASSIGN, // TK_EQ, // TK_LPAREN, // ( TK_RPAREN, // ) TK_LBRACE, // { TK_RBRACE, // } TK_SEMI, // ; TK_EOF, // 文件结束 TK_ERROR // 词法错误 } TokenType; typedef struct { TokenType type; // 这个 token 的种类 char lexeme[MAX_LEXEME]; // 词素原文 int line; // 所在行号从 1 开始 } Token;这段代码里的两个宏参数值得说明。MAX_LEXEME 设 64 对课程实验足够标识符一般不会超过 31 个字符如果你嫌小改成 128 没有任何副作用但要在报告的参数说明里写一句「缓冲区长度 64超长词素前 63 个字符保留第 64 位放 \0」不要让它变成隐形的 63 字符截断。MAX_TOKENS 是输出数组上限词法分析器是单遍扫描循环里检查一下当前输出数量是否到上限防止测试文件太大时数组越界这也是报告里可以提到的健壮性设计。3.2 状态转移主体跳过空白与注释再进入各 token 识别子状态主循环是整台分析器的心脏。我建议把四个动作分开写跳过空白、跳过注释、识别词素、查关键字表。下面这一段是核心Token tokens[MAX_TOKENS]; int scan(const char *src, Token *tokens) { int i 0; // 源串游标 int line 1; // 当前行号 int n 0; // 已输出 token 数 while (src[i] ! \0 n MAX_TOKENS - 1) { // 空白字符直接跳过 if (src[i] || src[i] \t) { i; continue; } if (src[i] \n) { line; i; continue; } // 单行注释 // 到行尾 if (src[i] / src[i1] /) { while (src[i] ! \n src[i] ! \0) i; continue; } // 多行注释 /* */注意内部换行要同步行号 if (src[i] / src[i1] *) { i 2; while (!(src[i] * src[i1] /)) { if (src[i] \0) { tokens[n].type TK_ERROR; tokens[n].line line; snprintf(tokens[n].lexeme, MAX_LEXEME, unclosed comment); return n; // 注释未闭合提前结束 } if (src[i] \n) line; i; } i 2; // 跳过分隔符 */ continue; } // 标识符与关键字先读词素再查表 if (isalpha(src[i]) || src[i] _) { int start i; while (isalnum(src[i]) || src[i] _) i; int len i - start; Token *t tokens[n]; t-type lookup_keyword(src start, len); t-line line; memcpy(t-lexeme, src start, len); t-lexeme[len] \0; continue; } // 数字字面量只接受十进制整数 if (isdigit(src[i])) { int start i; while (isdigit(src[i])) i; // 数字后紧跟字母/下划线属于 123abc 这种非法词素 if (isalpha(src[i]) || src[i] _) { tokens[n].type TK_ERROR; tokens[n].line line; memcpy(tokens[n].lexeme, src start, i - start); tokens[n].lexeme[i - start] \0; n; while (isalnum(src[i]) || src[i] _) i; continue; } Token *t tokens[n]; t-type TK_NUM; t-line line; memcpy(t-lexeme, src start, i - start); t-lexeme[i - start] \0; continue; } // 单字符运算符与分隔符 switch (src[i]) { case : push_token(tokens, n, TK_PLUS, , line); i; break; case -: push_token(tokens, n, TK_MINUS, -, line); i; break; case *: push_token(tokens, n, TK_STAR, *, line); i; break; case (: push_token(tokens, n, TK_LPAREN, (, line); i; break; case ): push_token(tokens, n, TK_RPAREN, ), line); i; break; case {: push_token(tokens, n, TK_LBRACE, {, line); i; break; case }: push_token(tokens, n, TK_RBRACE, }, line); i; break; case ;: push_token(tokens, n, TK_SEMI, ;, line); i; break; case /: push_token(tokens, n, TK_SLASH, /, line); i; break; case : if (src[i1] ) { push_token(tokens, n, TK_EQ, , line); i 2; } else { push_token(tokens, n, TK_ASSIGN, , line); i; } break; default: push_token(tokens, n, TK_ERROR, ?, line); i; } } // 源文件结束 tokens[n].type TK_EOF; tokens[n].line line; tokens[n].lexeme[0] \0; return n; }这段代码里最需要注意的是lookup_keyword的调用时机。 它没有在读到字面量的第一个字符时去判断而是在整个标识符词素读完之后按「字符串内容」查表这正是 2.3 节强调的关键字识别时机。如果你把查表动作提前到第一个字符输入ink会被误判为int开头的关键字并拆成两个 token这类 bug 在测试时会非常隐蔽。另一个值得在报告里写清楚的是TK_ERROR的处理方式。 错误 token 和被识别的 token 一起放进输出数组行号字段照常填写。这样做的好处是分析器不会在中途崩溃错误收集完还能继续扫描后面的内容报告里可以把「词法错误不中断扫描」写成设计决定而不是偷懒。3.3 错误处理与 main 函数报错信息怎么写才不扣分主函数负责读文件、调扫描器、输出结果。实验报告里 main 函数的代码不用多但参数设计要合理int main(int argc, char *argv[]) { if (argc ! 2) { fprintf(stderr, usage: %s input.c\n, argv[0]); return 1; } FILE *fp fopen(argv[1], r); if (!fp) { perror(fopen); return 1; } // 整个文件读进内存单遍扫描 fseek(fp, 0, SEEK_END); long size ftell(fp); fseek(fp, 0, SEEK_SET); char *src malloc(size 1); fread(src, 1, size, fp); src[size] \0; fclose(fp); Token tokens[MAX_TOKENS]; int count scan(src, tokens); for (int i 0; i count; i) { printf(line%d: %s(%s)\n, tokens[i].line, token_type_name(tokens[i].type), tokens[i].lexeme); } free(src); return 0; }在实验报告的写法上有一个小区别报错信息的格式不要写成自由文本建议统一输出成与正常 token 相同的行格式只是类型为 ERROR。比如line3: ERROR(123abc)这样老师一眼就能看出错误发生在第几行、错误词素是什么。错误信息里只输出行号和词素不输出「syntax error at line 3, unexpected character...」这种长句因为词法错误是「这个词不合法」不是「这句话不合语法」措辞错了会让助教觉得你概念没学扎实。3.4 如果你用 Java 实现HashMap 关键字表与 ArrayList 输出不少同学实验报告要求用 Java 重写思路完全一样只有语言细节不同。关键字表用HashMapString, TokenType输出用ArrayListToken主循环里同样是一个大 switch 或状态枚举。Java 版要注意别引入正则表达式库去识别 token——实验考察的是词法分析原理用 regex 一句话匹配掉标识符老师没法看到你的状态转移逻辑会直接扣分。如果确实用了正则也要在报告里把正则对应的 DFA 手工画出来这个工作量比直接写状态机还大没必要。4. 实验报告避坑指南测试用例边界与五个典型翻车场景4.1 关键字被识别成标识符分不清查表时机现象输入int a 1;输出结果里int的类型是 IDENTIFIER 而不是 KEYWORD。原因代码在读到字符i时就开始对int做字符串匹配匹配成功立即输出关键字。遇到ink这类以in开头的合法标识符时程序要么输出int加残余字符要么直接不匹配行为不可控。解决先把整个字母数字串读进缓冲区等落到标识符的接受状态之后再拿着完整词素去查关键字表。查表命中就是关键字查不到就是标识符。这条规则写进报告属于可以预判的常考追问点。4.2 最大匹配出现歧义123abc 到底该怎么断词现象输入a 123abc;有的程序输出123和abc两个 token有的程序把123abc整体当标识符还有的程序直接崩了。原因教材里的最大匹配原则有一个前提——匹配发生在同一类 token 内部。数字状态读到的字符仍然是数字就继续前进一旦遇到字母说明这个字符序列跨了数字和标识符两个类别这属于词法错误而不是「数字读完接着读标识符」。解决数字识别循环结束后检查下一个字符是否是字母或下划线。如果是把已积累的数字串标为 TK_ERROR行号照填然后跳过剩下连续的字母数字字符继续扫描。报告中可以写一句「词法错误收集后不中断扫描」这是加分点。4.3 多行注释在文件末尾未闭合行号与错误信息双双跑偏现象测试文件有 30 行最后一行多行注释少写了*/结束符。报错信息却显示line1: ERROR(unclosed comment)。原因注释扫描逻辑里line变量没有随换行符自增只在主循环里自增。多行注释内部遇到\n时直接跳过了行号一直停在注释开始的那一行。解决多行注释的 while 循环里每次src[i] \n都执行line扫描到 EOF 时记录的是文件真实末尾的行号。这条坑几乎每个班都会出现一次写进报告的测试用例部分能说明你对行号追踪这个细节有意识。4.4 固定缓冲区加 fread 一次性读入大文件测试时行为不对劲现象用fgets加固定char buf[128]逐行读入测试小文件没问题换一个几千行的文件token 输出错乱。原因词法分析的绝大部分坑来自「一行没读完就换行」和「行号漂移」。固定大小缓冲区逐行读行号由读取次数决定而不是由\n数量决定。如果一行超过缓冲区长度程序会以为读到两行行号翻倍。解决按第 3.3 节的方式把整个文件一次读进动态分配的内存单遍扫描行号全部由扫描器在遇到\n时自增从根上消除「读文件方式影响分析结果」这类玄学问题。用这个方案行号永远等于源文件真实行号报告里也能理直气壮地写「本实现不依赖操作系统的行缓冲」。4.5 报告里的 DFA 图与代码行为不符答辩被追问一次就穿帮现象报告里画了一张五状态 DFA图很漂亮但代码里是三层 if 嵌套状态个数跟图对不上。助教问「从标识符状态遇到数字跳到哪个状态」你看着自己的代码根本答不上来。原因图是写完代码之后补的为了排版好看照着教材改的代码五行一个状态都没有。这种情况在答辩时比代码报错更致命因为错误代码可以被解释成笔误图与代码不一致只能说明报告是编的。解决先画状态转移表再写代码。表格的行是状态列是字符类别表里的每一项是下一个状态。代码的 switch-case 结构照抄这张表报告里图、表、代码三段互相印证。回答助教提问时指着表格说「标识符状态遇到数字仍然留在标识符状态」逻辑完全闭环。5. 让报告多拿 5 分的验证技巧最小测试集、自查清单与答辩预演5.1 我建议的最小测试集一个文件覆盖全部状态与其写十个零散测试文件不如做一个test.c故意把合法与非法用例混排在一起。下面这个表是我常用的最小覆盖组合你可以直接抄进报告「测试用例」一节输入片段期望结果覆盖目标int main() {关键字、标识符、括号、左花括号依次输出基础路径if (a b) a 1;双等号应输出为 TK_EQ 一个 token运算符最大匹配while /* 跨行\n注释 */ (c)多行注释被跳过且不产生 token注释处理与行号x 123abc;123abc输出为 TK_ERROR非法词素检测return // 注释\n 0;单行注释后正常识别数字单行注释边界}空输入场景下的文件结束EOF token测试时记录一次实际输出和期望输出的差异哪怕是全对也要在报告里写一句「以上六个用例全部通过」。这个动作的性价比极高——它直接告诉老师你的程序被验证过而不是编译通过就完事。5.2 交报告前十分钟的自查清单还有一份自查清单每条对应一个扣分点。报告里是否包含输入输出示例各一个代码中的 token 类型表是否与报告正文章节统一DFA 图是否与代码状态一一对应错误 token 的输出格式是否和正常 token 一致最多 token 数上限是否写在了参数说明里。这五条检查完报告在形式上是完整的。说个我自己的教训。当年我交实验报告代码跑得分毫不差但 DFA 图是从网上找的跟自己的实现差了两个状态。答辩助教随口问了一句「标识符状态遇到数字怎么办」我当场指着图编了一个转移代码却根本不是那么走的。那次被扣了分不冤。后来每一份报告我都坚持一个原则图、表、代码三样东西必须互相能对上哪怕丑一点也要诚实。编译原理实验报告1这个题目难点从来不在算法复杂度而在状态转移的严谨和报告的可复现性。把状态机写清楚把测试集跑干净把图和代码对齐这门实验的分数不会辜负你。希望帮到你。本文还有配套的精品资源点击获取