简介:西南科技大学编译原理实验报告,主题为设计词法分析程序,适合计算机专业学生、备考者及需要完成编译原理实验的学习者参考。报告完整呈现实验目的、实验设计、实验过程与程序实现,涵盖正则表达式描述词法规则、非确定有限自动机构造与合并、确定化并化简为最小确定有限自动机等关键环节,同时给出标识符、保留字、无符号整数、分界符、运算符、注释符等单词分类及输出方案,并附有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-10 | if(1) else(2) while(3) for(4) int(5) return(6) void(7) break(8) continue(9) main(10) |
| 标识符 | 11 | 用户自定义的变量名、函数名 |
| 整型常量 | 12 | 123、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序列 | 常见错误结果 | 原因分析 |
|---|---|---|---|
ifx=1 | ifx(标识符)=(运算符)1(数字) | if(关键字)x(标识符) ... | 关键字匹配必须基于完整单词,不能用前缀匹配 |
1abc | 1(数字)abc(标识符) | 报错“非法字符”或整体拒绝 | 扫描到a时数字状态结束,回退后按标识符规则继续 |
a==b | a==b | a==b | ==是双字符运算符,必须预读两个字符再决定 |
/* comment */ | 不产生token(跳过) | 把/和*分别输出 | 注释处理要在扫描器层消化掉,否则会把注释内容误分析 |
这张表是报告里的“含金量”所在——它说明你不仅跑完了程序,还理解了自己写的每一段判断是干什么的。
5. 实验报告避坑指南:5个高频翻车点,现象、原因、解决一次说清
5.1 关键字的识别顺序写反,ifx被拆成if和x
现象:输入ifx=1,程序输出的是关键字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,说到底是一道“能否把理论课的状态转换图变成可运行的扫描器”的开放题。实验报告是记录这次转换过程的最佳载体。如果读完这篇你还有犹豫,建议从状态转换图开始画起,图出来了,代码和报告各完成了一半。希望这篇能帮你有惊无险地过关。
本文还有配套的精品资源,点击获取