十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

编译原理实验一:词法分析器从状态转换图到代码实现指南

编译原理实验一:词法分析器从状态转换图到代码实现指南 简介湖南大学编译原理实验一的完整资料包面向湖大计算机相关专业本科生的编译原理课程实验场景旨在解决实验报告撰写与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的时候放弃。最后再分享一个我自己的习惯每次做这种状态机类实验我都会在纸上把状态转换图和几个关键边界样例写清楚再动手。看起来像是浪费时间实际能让你少熬一个通宵。你在做实验一时培养起的这种“先建模、再编码”的思维方式后面几个实验会更受益。本文还有配套的精品资源点击获取
返回列表