
简介一份编译原理课程实验报告源自东北大学秦皇岛分校计算机与通信工程学院主要围绕PL/0语言词法分析程序的设计与实现展开适合正在学习编译器原理的高校学生作为课程设计或实验参考。报告完整包含实验目的、实验环境、程序设计与调试过程核心部分是C编写的词法分析器完整源码详细实现了isLetter、isDigit、isP、isJ、isBlank等字符类别判断函数并通过关键词表比对将源程序转换为单词类别二元组序列同时具备对“2A”这类非法输入的检测能力。压缩包内共1个doc文档约122KB文档层次清晰涵盖源代码、运行输出及必要注释便于对照学习和二次修改。目前已有98人学习下载对于希望掌握词法分析基本流程、练习文本处理与错误检测编码技能的初学者来说具有直接的参考价值。通过研读这份报告还能理解从读取pl0.txt源文件到逐字符扫描、单词识别、分类输出的完整流程为后续学习语法分析、语义分析打基础。1. 编译原理实验报告的开题词法分析与递归子程序两道坎一份编译原理实验报告最容易卡死的不是写结论而是词法分析和递归子程序分析这两个程序怎么从“代码能编译”变成“原理能讲清”。某次课程要求做一个 PL/0 语言的词法分析程序输入是源程序字符串输出是单词、类别二元组序列并且要有检查错误的能力比如发现2A这类非法单词另一道题是递归子程序分析器判断任意符号串是否属于文法G[E]生成的句子。这两个实验在 Windows 上用 C 完成看起来一个偏字符处理、一个偏文法推导实际放到同一份方案里正好覆盖了编译前端的两块核心。这篇方案就按“需求背板 → 总体设计 → 关键实现 → 常见问题 → 测试验收 → 答辩技巧”的顺序展开适合正在赶编译原理课设的学生也能直接给从业者做演示程序时参考。2. 词法分析程序总体设计字符分类、类别码表与主循环词法分析本质上就是四个动作读字符、归类、拼词、查表。整个 PL/0 词法分析程序把源文件读成单字符流用pos下标逐个推进每遇到一个字符先判断它是字母、数字、运算符、界符还是空白符然后按不同分支“贪心”读取后续字符拼出一个完整单词再到预定义的类别码表里查它到底是关键字、标识符还是无符号数。这个方案没有用状态机而是用“字符分类函数 表驱动”来简化实现实验报告里解释起来非常直白答辩时也容易给老师讲清楚。2.1 字符分类函数isLetter、isDigit、isP、isJ、isBlank 的职责边界先看字符判断层这五个函数是词法分析器能工作下去的地基。bool isLetter(char ch) { return (ch A ch Z) || (ch a ch z); } bool isDigit(char ch) { return ch 0 ch 9; } bool isP(char ch) { // 算术运算符 return ch || ch * || ch - || ch /; } bool isJ(char ch) { // 界符与关系运算符 return ch , || ch ; || ch . || ch ( || ch ) || ch [ || ch ] || ch || ch : || ch || ch || ch { || ch } || ch #; } bool isBlank(char ch) { return ch || ch \t; }注意isP只覆盖 - * /四个算术运算符而isJ覆盖逗号、分号、括号、冒号、尖括号这些符号。这样分开定义不是为了写起来好看而是为了在主循环里确立判断顺序先看数字、再看字母、再看运算符、最后看界符。PL/0 语言里标识符和常数出现频率最高把它们放在最前可以减少无效判断同时、-这种算术运算符不会掉进界符分支避免isJ把 -误拼成一个符号串。字符分类函数尽量不要合并实验改起来也方便比如想扩充一个下划线_只改isLetter一处即可。2.2 关键字表与类别码47 项表项的映射与查询逻辑字符判断做完之后还需要一张表把单词映射成类别码。原实现里定义了一张 47 项的字符串表前 21 项是保留关键字接着是标识符和无符号数再往后是界符和运算符。char ktt[48][20] { and, begin, const, div, do, else, end, function, if, integer, not, or, procedure, program, read, real, then, type, var, while, write, 标识符, 无符号数, ,, ;, :, ., (, ), [, ], .., , --, , -, *, /, , , , , , , :, {, } };类别码的映射规律是0~20为关键字and是 0write是 2021是标识符22是无符号数23~46是界符与运算符。这个映射表一旦定下来词法分析器后面所有输出都依赖它所以写实验报告时最好把这张表完整贴出来再配一行文字说明“单词与类别码的对应关系是词法分析的输出契约”。查询实现采用最朴素的strcmp线性查找扫描表里每一项做字符串比较关键字总共不超过 21 个符号表最长也就 47 项在课程设计的源程序量级上线性查找性能完全够用而且比用map更容易在报告里解释查找次数和匹配过程。提示类别码表是词法分析器最核心的“黑匣子”答辩时老师常会指着某个输出问“这个 44 是哪来的”能立刻答出:的类别码是 44就说明真的理解了表驱动机制。2.3 主循环状态推进单字符前瞻与单词拼装的实现类别码表准备好以后主循环负责把源文件pl0.txt的内容逐个字符吃进去拼出单词查表并把类别码累积到ste字符串里遇到换行就输出一行类别码。核心逻辑如下int pos 0; char ch src[pos]; while (ch ! #) { if (ch \n) { cout ste endl; // 输出当前行的类别码串 ste ; ch src[pos]; continue; } if (isBlank(ch)) { ch src[pos]; continue; } if (isDigit(ch)) { // 无符号数 int i 0; char str[20] {0}; while (isDigit(ch) || ch .) { str[i] ch; ch src[pos]; } ste |22; // 无符号数类别码 cout str; continue; } if (isLetter(ch)) { // 标识符或关键字 int i 0; char str[20] {0}; while (isLetter(ch) || isDigit(ch)) { str[i] ch; ch src[pos]; } // 在 ktt 表 0~20 中找关键字找不到则类别码为 21 ste |21; cout str; continue; } // 运算符、界符分支省略处理思路相同 ch src[pos]; }这段代码里的src是读入的源程序字符串末尾人为追加#作为终止符pos是当前读到的字符位置ch是当前字符。ste保存的是类别码串比如|0|21|38|22遇到换行才整体输出这样源程序每一行都对应一行类别码报告里截图非常直观。这里的关键设计是“单字符前瞻”每次读超过一个字符时比如拼标识符COUNT循环读到不是字母也不是数字的位置才停而这个“停下来的字符”不丢失留到下一轮再分类这是词法分析不出错的最重要习惯。原实验代码在不做额外检查时输入2A会被拆成数字2和标识符A但题目要求的是发现非法单词所以需要在数字分支结束后补一个判断这一步放到后面常见问题里详细说。3. 递归子程序分析器设计文法 G[E] 到四个递归函数词法分析把字符串切成单词后语法分析要判断这些单词能不能组成合法句子。实验 4 要求用递归子程序法实现文法G[E]的句子识别。递归子程序法的核心思想是“一个非终结符对应一个函数”函数体照抄产生式右部遇见终结符就与当前输入字符比对遇见非终结符就调用对应函数。本项目只有四个非终结符E、A、B、C代码结构非常清晰适合作为递归下降分析的入门样例写进报告。3.1 文法拆解E→eBaA 的产生式到函数骨架要分析的文法如下G[E] E - eBaA A - a | bAcB B - dEd | aC C - e | dC这个文法最大的特点是各候选式之间不存在二义性冲突A的两个候选a和bAcB首字符分别是a和b不会撞车B的两个候选dEd和aC首字符是d和aC的两个候选e和dC首字符是e和d。所以分析器不需要回溯读一个当前字符就能决定走哪个分支这就是递归下降能在这里成立的前提。把产生式翻译成函数骨架E先匹配e然后调B再匹配a最后调AB按当前字符决定是走dEd还是aCC则根据e或d决定是直接结束还是继续递归。特别注意E和B之间存在间接递归E调BB在dEd分支又调E。这个特点决定了四个函数必须先声明后定义不然编译阶段直接报错。3.2 递归函数实现A、B、C 的间接递归调用下面给出可直接编译的递归子程序分析器完整代码修正了原稿里几个隐蔽问题并加了行内注释方便写报告时引用#include stdio.h #include string.h char a[100]; // 存放输入串 int s 0; // 当前读到的字符位置 void E(); void A(); void B(); void C(); void E() { if (a[s] e) { // 匹配 e s; B(); // 非终结符 B if (a[s] a) { // 匹配 a s; A(); // 非终结符 A } } } void A() { if (a[s] b) { // 候选 bAcB s; A(); if (a[s] c) { s; B(); } } else if (a[s] a) { // 候选 a s; } } void B() { if (a[s] d) { // 候选 dEd s; E(); if (a[s] d) { s; } } else if (a[s] a) { // 候选 aC s; C(); } } void C() { if (a[s] d) { // 候选 dC右递归 s; C(); } else if (a[s] e) { // 候选 e s; } } int main() { while (1) { printf(请输入算数表达式(以#键结束)); scanf(%s, a); s 0; // 每次分析前重置游标 E(); if (a[s] #) printf(句子结构正确\n); else printf(分析失败\n); } return 0; }对照文法看这段代码E的函数体就是eBaA的逐符号翻译先看当前字符是不是e是则推进游标调用B()再看下一个字符是不是a是则推进游标调用A()。A内部先判断b然后递归调用A再匹配c、调用B如果首字符是a就直接吃掉。整个程序没有显式错误恢复某一步匹配失败时函数直接返回主函数发现游标没停在#就判定分析失败这正是递归下降最朴素也最好解释的判别方式。需要特别注意两个地方一是while(1)在 C 中不能写成While(1)Windows 下编译报错时会提示While未定义花两秒改掉就好二是每次输入换新的测试串前必须把s重置为 0否则第二次分析会从上次停下的位置继续判断结果全是“分析失败”。3.3 句子判别主控输入串读取与 # 结束符判定主控逻辑非常短但它是整个分析器的“裁判”读入一串以#结尾的符号调用E()从起始符号开始推导如果分析结束后游标正好停在#说明整条串被完整消费掉句子合法否则说明某个位置匹配不上输出“分析失败”。这种写法的好处是输出极其简单实验结果只需要三行输入串、分析结果、游标停留位置。做实验报告时截图上最好把a[s]停在哪一步也打印出来比如输入eaa#时E匹配e后调BB匹配a后调CC不匹配最终a[s]停在第二个a上这个第二个a就是断言失败的位置。能把“停在哪个字符”讲清楚比只贴一句“分析失败”更有说服力。4. 常见问题与排查误判、串读与栈溢出的翻车点这份实验报告的程序我实际改过好几轮最容易反复出事的就是下面四个点。每条都按“现象 → 原因 → 解决”来写实验过程中直接对照排查。4.1 翻车点一2A 被拆成两个合法单词现象输入2A词法分析器输出数字2和标识符A完全两个合法单词题目要求的“发现非法单词”等于没做。原因数字分支在读完整段数字后立刻收手没有检查紧跟的字符是否还能作为标识符的一部分。解决在数字分支的while循环结束后紧跟着判断当前chif (isDigit(ch)) { // ... 拼数字 if (isLetter(ch) || ch _) { printf(非法单词%s%c\n, str, ch); ch src[pos]; // 跳过非法字符 continue; } }这样2A会被当场拦截而不是被拆成两个“正常单词”。这是词法分析器“错误检查能力”最直接的体现写报告时单独列一条“非法字符串识别”是加分点。4.2 翻车点二连续界符 被整段吞掉现象输入AB输出里A、B都在但两个等号消失词法分析器像没看见它们一样。原因isJ分支用while(isJ(ch))一口气读取连续界符会被拼成一个字符串而类别码表里没有这一项查表失败后既没输出单词也没输出错误整个串被静默丢弃。解决PL/0 的最长复合符号是:、、、这类两字符组合所以isJ读取时最多读两个字符读到第二位就停下单字符也能单独查表。更稳妥的做法是查表失败时不吞字符把pos回退到当前串的起始位置让下一个字符重新进入主循环分类。4.3 翻车点三递归函数未提前声明导致编译失败现象代码写完后编译报错提示B未定义。原因E()的定义在B()前面而E()内部调用了B()C 要求标识符先声明后使用函数定义顺序没对上就报错。解决在文件顶部加四个函数前置声明void E(); void A(); void B(); void C();这是递归子程序代码的固定动作不属于“写错”而是 C 编译规则。实验中很多学生卡在这一步报告里可以写一句“因递归调用需提前声明函数原型”反而显得懂原理。4.4 翻车点四pl0.txt 缺失或内容为空时静默失败现象运行词法分析程序后控制台一片空白没有任何输出看起来像死机。原因fopen(pl0.txt, r)成功与否没有检查文件不存在时fgetc立刻返回EOF读取循环直接结束后面自然什么都不输出如果文件是空文件读完后只有末尾追加的#主循环一次都不执行。解决fopen后立刻判空失败时打印错误提示并返回读取结束后判断src是否为空如果只有#就输出“源文件为空”。这两步虽然不起眼但能让程序在异常输入面前不至于“装死”验收时也会被当作工程严谨性的一项。5. 测试用例与实验结果pl0.txt 全流程复现方案写完必须用测试用例证明它能跑。测试分两层词法分析用pl0.txt作为标准输入递归子程序分析用一组合法和非法句子做断言。把预期输出和实际输出并排贴进实验报告是最直接也最有效的验收方式。5.1 测试用例设计关键字、标识符、常数与复合运算符覆盖词法分析的测试文件故意设计成这样用来一次覆盖多个分支CONST A10; VAR B,C; 2B PROCEDURE P; VAR D; BEGIN READ(X);D:X;WHILE X0 DO CALL P; END.这里面CONST、VAR、PROCEDURE、BEGIN、READ、WHILE、DO、END覆盖关键字A、B、C、P、X覆盖标识符10、0覆盖无符号数;、,、.覆盖界符:覆盖复合运算符2B专门用来触发非法单词检查。期望的类别码序列如下表输入片段期望类别串说明CONST A10;CONST(2) A(21) (38) 10(22) ;(24)关键字、标识符、等号、常数、界符VAR B,C;VAR(18) B(21) ,(23) C(21) ;(24)逗号和分号分别归类D:XD(21) :(44) X(21)复合运算符应整体识别WHILE X0 DOWHILE(19) X(21) (39) 0(22) DO(4)关系符紧跟常数不粘连2B输出“非法单词2B”改进版错误检测生效递归子程序分析的测试用例更简单合法句子输入eaeaa#预期输出“句子结构正确”非法句子输入eaaa#预期输出“分析失败”。测试时用一个极小样例就够没必要跑大段代码因为递归下降的判定只取决于当前位置的字符匹配样例越小越容易定位问题。5.2 实验结果核对类别码序列与预期输出对照核对实验结果时最常遇到的问题是类别码对不上。比如把:输出成:(25) (38)说明isJ分支没有按最长符号优先匹配或者在查表时匹配到了单字符项把无符号数输出成实际数值而非类别码22说明输出逻辑里混用了单词内容和类别码。我一般会先在报告里画一张“源行 → 类别码行”的对照截图然后在代码里对每一个分支标注类别码来源这样对不上时一眼就能看出是查表顺序还是累加逻辑的问题。实验里所有无符号数的类别码统一是22不区分数字大小这是很多人容易忽略的点。5.3 可复现运行流程Windows 命令行编译与批量测试为了让验收的人能原样跑通把编译运行步骤写清楚比贴一段代码更有价值。Windows 下打开命令行进入源文件目录依次执行g lexer.cpp -o lexer.exe lexer.exe词法分析器默认读取当前目录下的pl0.txt控制台会先输出源程序全文再按行输出类别码。递归子程序分析器编译运行后进入交互模式直接输入测试串加#回车即可。批量测试时可以用命令行重定向把多个测试串写进test_cases.txt然后执行analyzer.exe test_cases.txt一次跑完所有用例避免手动逐个输入。这一条写进报告的实验环境说明里会显得整套方案具备可复现性而不是只能看截图。6. 验收与答辩从能跑通到能讲清6.1 验收清单实验报告八项自查提交前按下面这份清单过一遍能避免绝大多数低级扣分词法分析能输出单词、类别或类别码行2A这类非法串能被拦截而不是拆成两个合法单词:、这类复合符号被识别为整体类别码分别是44、42pl0.txt缺失时程序给出明确提示递归子程序分析器对eaeaa#判断正确对eaaa#判断失败每次输入新串前s都重置为 0代码文件有四个递归函数的前置声明实验报告包含实验目的、设计思路、完整代码、测试用例、结果截图五部分。这八项里前六项直接跑测试用例就能验证后两项靠查代码和翻报告就能确认。6.2 答辩主线用一组最小样例把两个实验串成编译前端答辩时不要把词法和语法当成两个孤立程序一句话就能把两者串起来词法分析得到的单词、类别二元组序列就是递归子程序分析器要消费的输入流前者负责切词后者负责判断切出来的词是否满足文法。现场演示用最小样例就够了词法部分输入CONST A10;输出CONST(2) A(21) (38) 10(22)语法部分输入eaeaa#输出“句子结构正确”。讲词法时强调字符分类、最长匹配、类别码表三层结构讲语法时强调产生式到函数体的逐条翻译以及为什么这个文法不需要回溯。我自己的一个习惯是每份编译原理实验报告在提交前都先留一份「预期结果对照表」代码改了哪一行对照表就同步更新哪一行。有过一次报告里截图和代码行为完全对不上临时慌着重跑所有用例的经历之后我就再也不敢跳过这步了。希望这套方案能帮到你的实验顺利收尾。本文还有配套的精品资源点击获取