简介:湖南大学编译原理实验一的完整资料包,面向湖大计算机相关专业本科生的编译原理课程实验场景,旨在解决实验报告撰写与DFA代码实现过程中的思路参考问题。整份资源打包为zip格式,体积仅764KB,内部共7个文件,包含4个dfa自动机定义文件、DFA_1.cpp源码、实验报告docx以及可执行的exe程序,代码、文档与运行验证一应俱全。已有708人学习下载,既是热门参考,也从侧面说明其实用性。资料特别适合希望在实验中获得高分、但又缺少完整实现框架的同学;其中DFA的构建与处理逻辑、实验报告的结构化呈现,以及可直接运行的exe程序,都能帮助读者快速理解实验要求、对照检查自身代码与报告。需要提醒的是,描述中作者亦强调代码与报告仅供思路参考,建议在理解基础上独立完成,才能真正掌握编译原理DFA部分的核心知识。 拿到这个压缩包的时候,我第一反应是:又一份编译原理实验的“传家宝”。每年都有人从学长学姐那里拷贝到一份《湖南大学 编译原理实验一.zip》,解压之后对着里面的模板代码和实验指导书发懵,不知道从哪下手。这篇文章我就以这份实验一为例,把解压之后该看什么、实验一到底在考什么、代码怎么才能跑通、老师批改时盯着哪些细节,一次讲清楚。不管你是正在修这门课的学生,还是准备考研复试想补一补编译基础,甚至是自学编译器想找一条入门路径,这篇都有参考价值。
1. 解压之后,先别急着跑:看懂实验一压缩包的标准结构
我在多个不同学校版本的编译原理实验课里见过类似的压缩包,结构上大同小异。你解压后大概率会看到这么几类文件:实验指导书(PDF或Word)、一个带残缺代码的工程目录或用例说明。第一次打开的时候,很多人直接点开代码文件就开始改,这种做法其实效率很低。正确顺序是先把指导书通读一遍,搞清楚这次实验要求你提交的是什么。
通常实验一的任务集中在词法分析上。指导书里会给出一个文档,说明要识别的token类别:关键字(比如void、int、if、else、while、return)、标识符、整数常量、运算符(+、-、*、/、=、==、!=、<、>)、界符(括号、分号、逗号),以及要求跳过的空白符和注释。有些版本还会要求把符号插入符号表。你需要的“编译原理符号表”这个题目,在高阶实验里会更深入,但实验一一般只是做一个建表入口。
压缩包里的模板代码通常是一个或多个源文件——有的给的是C语言框架,有的给的是C++,湖大这个版本我见过好几次是纯C的实现。模板里一般已经有了主循环的雏形:读入源程序、调用你实现的词法分析函数、输出token序列。你要做的不是推翻重写,而是在这个骨架上补全“从输入流里读一个token并判定其类型”的那部分逻辑。先花半小时把文件结构和指导书要求摸清楚,比直接开改代码更值得。
另外,解压后的文件路径里尽量不要出现中文。我见过太多次因为中文路径导致读取文件失败、或者编译器报错找不到头文件的情况,这种问题在实验课上一抓一大把,纯属环境问题而不是代码问题。
2. 为什么几乎所有高校的编译原理实验一都锁在词法分析上
很多初学者会问:编译原理不是又难又抽象吗,为什么实验一只做“读字符、分类、输出”这种看起来毫无技术含量的事?这就要回到编译器的整体结构来看。一个完整的编译器大致分为词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成几个阶段。词法分析是第一步,它的任务就是把源程序里的一长串字符切分成一个个有意义的“词”,也就是token。
打个比方,你去读一篇英文文章,第一步不是去分析句子语法,而是先把单词切出来。编译器也一样,它得先知道哪里到哪是一个标识符,哪里到哪是一个数字,哪里是一个运算符,然后才能谈得上“这句话是什么结构”。词法分析器的输出,就是给语法分析器喂的一串token流。这个切词过程看似简单,但涉及一类核心概念:状态转换图,或者叫有限自动机。
实验一考察的能力,第一是你能不能把一个字符一个字符读入的过程模拟成状态转移,第二是你对“边界条件”的意识——什么时候一个token结束、下一个字符该怎么处理、读到文件末尾怎么办。这些在很多语言课里不会仔细讲,但恰恰是理解一切编译后续步骤的基础。
顺带说一句,很多人搜“编译原理 简答题 简述逆波兰式”这种题,会误以为实验一也要处理逆波兰式。其实逆波兰式是表达式求值和中间代码阶段的内容,实验一根本不会涉及。你在网上搜到逆波兰式的实践题,通常是后端实验或期末考试内容,别把它们混进当前实验来写,否则很容易给自己加戏、浪费半天时间。
词法分析还有一个隐藏考点:符号表。它通常在指导书的后半部分出现,要求你每识别一个标识符就查一下符号表,如果不存在就插入记录它的名字、类型、所在行号等信息。实验一阶段你可以用一个简单的数组或链表来实现,不用搞哈希表。但你要理解符号表是后续语义分析阶段共享数据结构的入口,现在写好一个清晰的结构体,之后几个实验都能复用。
3. 词法分析器跑起来的完整链路:从正则到状态转换图再到代码
这一节是全文最该细读的部分。我按三步讲解,你照着这个顺序做,实验一的代码量不大,但是逻辑能非常清晰。
3.1 第一步:把识别规则描述成状态转换图
你手里的实验指导书,一定有一节是“token的构成规则”。这些规则本质上就是正则表达式。比如:
- 标识符:字母开头,后跟字母、数字或下划线,长度不限。
- 无符号整数:一串数字。
- 关系运算符:
<、<=、>、>=、=、==、!=。
这些正则表达式可以直接翻译成状态转换图。拿“无符号整数”来说,状态0是开始,遇到数字到状态1;状态1里再遇到数字仍留在状态1;一旦遇到非数字字符,就终结当前token,回到状态0开始识别下一个token。标识符的状态图类似,只是多了字母、数字、下划线的区分。
这一步为什么重要?因为如果你不画图,脑子里很容易在几个分支之间绕晕。画状态转换图的好处是在动手写代码之前,你就把“每个状态下读入每个字符应该做什么”全部定义清楚了,写代码只是机械翻译。我在实际教学里观察到的规律是:凡是先画图再写代码的人,调试时间基本能减少一半以上;凡是上来就一路写 if 的人,大概率要在一堆嵌套分支里转圈。
3.2 第二步:两种实现方式怎么选
有了状态转换图,实现方式有两种主流路线。
第一种是手工编码实现。简单说就是用一个变量记录当前状态,在while循环里不断读入下一个字符,然后根据当前状态和读入字符的组合去决定新的状态和动作。这种方式的好处是代码直观、运行时效率高,适合实验一这种几十行的规模。坏处是如果你把自己绕进特别多的分支里,代码会变得很乱,所以请你一定先维护好注释和状态常量。
第二种是表驱动实现。你先建立一个二维转移表,行是状态,列是输入字符类别,表格的单元格里写着下一个状态编号。主程序只需要一个查表动作就能完成状态跳转。这种方式更接近真实工业级编译器的做法,代码结构也更优雅,但对初学者来说,建表本身容易出错,调试的时候也不容易定位,我一般不建议刚接触的人一上来就用表驱动。
我个人推荐在实验一阶段用手工编码方式,把状态转换图中每个终态对应的识别动作(切分token、回退字符、输出token类型)直接落在代码里,这样逻辑最透明,也最容易跟指导书上的要求对应。如果你的目标是参加竞赛或者做高阶项目,等理解了状态转移之后再迁移到表驱动也不迟。
3.3 第三步:主循环和边界条件的代码骨架
下面我给一个简化的C语言代码骨架,描述核心逻辑。注意这只是示例,具体类名、函数名要和你拿到的模板保持一致。
int nextToken(char *buf, int *start, int *end) { // 从buf[*start]开始扫描,识别一个token // 返回token类型,将token的起止位置写入start/end int state = 0; int i = *start; while (1) { char c = buf[i]; switch (state) { case 0: if (isalpha(c) || c == '_') state = 1; else if (isdigit(c)) state = 2; else if (c=='<' || c=='>' || c=='!' || c=='=') state = 3; else if (c=='+' || c=='-' || c=='*' || c=='/') state = 4; else if (c=='(' || c==')' || c==';' || c==',') state = 5; else return TOKEN_UNKNOWN; break; case 1: if (isalnum(c) || c == '_') state = 1; else { *end = i; return TOKEN_IDENTIFIER; } break; case 2: if (isdigit(c)) state = 2; else { *end = i; return TOKEN_NUMBER; } break; // ... 其他状态 } i++; } }注意看,状态1下,如果读到的字符不能延续标识符,我并没有让i前进,而是直接返回,把end设置在当前位置。这就是“回退一个字符”的核心:当前字符不是本token的一部分,它可能是下一个token的开头,所以你的指针不能多往前走。这个细节非常容易踩坑,很多人第一次写的时候让指针直接越过那个字符,结果输出里就会莫名丢字符或者token错位。
文件结束的处理也不能漏,通常用判断c == '\0'或者c == EOF来作为流结束标志。在文件结束时,如果当前状态处于某个终态,要先返回当前的token;如果处于开始状态,才返回EOF token。千万不要在读到末尾时还把未识别的符号硬拼进上一个token。
还需要对空白字符和注释做跳过处理。空白的处理比较简单:在开始状态下遇到空格、换行、制表符就直接跳过。注释的跳过要讲究:/*注释里可能跨行,//注释只到行尾。字符串里的//不能当注释跳过,真真实实写的时候,需要在字符串状态下专门判断,否则调试时会在测试例上翻车。
4. 把代码跑通的关键步骤:从编译到对照测试
说一个很多人不知道的事实:实验课老师批改的时候,往往不是人眼一行行看你的代码,而是用脚本跑一组测试用例,把你的输出和标准答案做diff。所以能不能编译通过、输出格式对不对、有没有多余回车和空格,直接决定你的分数。
4.1 编译环节的排查
先确保你的环境能编译模板。我见过反复出现的问题有几个:C++模板文件在纯C编译器下编译不过;头文件路径因为工程目录迁移而失效;代码里用了非标准库函数(比如itoa),导致在Linux环境下编译报警告。建议你解压之后先用原始模板尝试编译一遍,确认环境没问题再动代码。编译命令就两行:
gcc -o lexer main.c lexer.c -Wall ./lexer test.c如果编译有报错,别慌,优先看错误信息里的文件名和行号。第一次编译最常见的错误是漏了某个头文件、少了个分号、函数声明顺序不对。把模板代码本身跑通了,你后面改动的任何编译错误都更容易定位——因为你知道之前是好的。
4.2 测试用例要分梯度
跑通代码之后,不要只用一个文件测完就宣布完工。我建议你建三个梯度:
- 基础用例:只有关键字、标识符、整数、简单运算符和分号。用来确认主流程没有问题。
- 边界用例:包含连续运算符(
<=、==、!=)、数字后紧跟字母(比如123abc,这应该是两个token:数字123和标识符abc)、行末注释、多行注释。 - 错误用例:比如非法字符
@、#,看看你的程序是报错还是错误地吞掉。指导书会要求“发现非法字符报错”,你要按它的要求来处理。
有一个技巧可以大幅减少调试时间:把预期输出的token序列写成一个文本文件,然后用diff命令跟你的输出比对。不要用眼睛一行行看,眼睛会骗你,尤其是空格和换行。我第一次调实验时就吃过这个亏——自己对着屏幕看了十分钟觉得一模一样,一跑diff全是差异,最后发现是每行末尾多了个空格。
4.3 调试时的万能手段
词法分析器的调试,最高效的手段是打印状态。你可以临时在每个case入口加一句printf("state=%d char=%c\n", state, c),然后小范围跑一段输入,逐个状态核对是否跟你的状态转换图一致。这个方法虽然土,但定位逻辑错误非常快。用GDB断点调试虽然高端,但对于状态机这种“边读边走”的逻辑,反而是打印状态更容易看到全貌。
如果你只会用IDE里那种点断点、单步走的方式,也可以,但记住:重点看“回退字符”和“token结束”这几个分支,绝大多数逻辑错误都藏在这里。
5. 那些实验文档不会直接告诉你、但决定分数的坑
最后一个部分,分享几个我看过无数学生踩过的坑,都是我这些年从实际批改和交流中总结出来的,尤其适合“拿到学长代码想直接抄”的情形。
5.1 关键字识别必须走在标识符识别前面
这是个顺序陷阱。当你用状态转换图识别出一个单词后,首先要查一下它是不是关键字,然后才能决定它是标识符还是关键字。很多人的代码只判断了“字母开头、字母数字下划线连续”就返回标识符,结果int被识别成了标识符——整个测试输出必挂。正确做法是识别出完整单词后,先查关键字表,命中就返回关键字类型,否则才是标识符。
5.2 输出格式里的空格和换行是隐形扣分点
自动化脚本比对输出时,严格要求每个token之间以固定分隔符(通常是空格或换行)隔开。实验指导书里可能会写“输出格式为每行一个token”,但很多人不细看,自己按喜好输出,然后diff一片红。拿到指导书后第一件事就是看输出样例的格式,对着校准。
5.3 注释和字符串内部的注释标记不能误判
//在字符串内部时它是普通字符,不是注释开始。同理,/*在字符串里也不是多行注释。处理方式是在状态0识别到双引号时,进入一个“字符串状态”,在这个状态下连续读字符直到遇见下一个双引号为止,期间不判断任何注释标记。这样就能避免误杀字符串内容。
5.4 看懂学长代码的正确姿势
如果你选择站在前人的肩膀上,直接打开别人的源码学习,我建议执行三步:第一步,编译运行,用简单用例确认它能跑;第二步,找到主循环,理清token读取的入口;第三步,找关键字、标识符、数字这几个分支,理解它的状态转移是怎么实现的。不要从文件开头逐行通读,那样90%的人会在看到第五十个if的时候放弃。
最后再分享一个我自己的习惯:每次做这种状态机类实验,我都会在纸上把状态转换图和几个关键边界样例写清楚再动手。看起来像是浪费时间,实际能让你少熬一个通宵。你在做实验一时培养起的这种“先建模、再编码”的思维方式,后面几个实验会更受益。
本文还有配套的精品资源,点击获取