
简介这份资源是湖南大学编译原理课程实验一的配套资料包面向正在修读该课程、需要完成DFA相关实验的本科生尤其适合想参考完整实现思路与报告写法的同学。包内共7个文件以4个dfa数据文件为核心搭配1个cpp源码、1个docx实验报告和1个exe可执行程序压缩包约764KB体量轻便便于快速下载与本地运行验证。目前已有709人学习下载说明在同类课程资料中具有一定参考热度。内容围绕DFA的构造、输入输出与程序实现展开cpp源码展示了核心算法逻辑dfa文件可作为测试用例直接运行docx报告则提供了实验过程与结果分析的写作框架exe文件方便无编译环境的同学直接观察程序行为。整体适合作为实验起步阶段的对照参考帮助理解状态转换与自动机实现细节但建议在参考基础上独立完成避免直接照搬。1. 湖南大学编译原理实验一从词法分析器开始把理论课欠的账一次还清如果你正在搜“湖南大学 编译原理实验一.zip”大概率是两种情况要么你拿到了这个压缩包但打开之后不知道从哪下手要么你还没拿到想先搞清楚实验一到底要做什么、值不值得花时间认真做。我当年带学弟做这个实验的时候最常见的场景是——理论课刚讲完正则表达式和有限自动机作业也勉强能写但一打开实验要求就懵了老师给了一段类似 C 语言子集的源代码要求输出 token 序列可课本上根本没教你“怎么把正则表达式变成能跑的代码”。实验一的核心就是词法分析器有的年份也叫 scanner 或 lexer。它要做的事情很具体读入一段源程序字符流按照语言定义的单词规则切分成一个个有意义的记号token比如关键字int、标识符count、运算符、界符;同时要能识别整数常量、浮点数常量还要跳过空白和注释。听起来简单但真正动手你会发现坑不少标识符和关键字的区分、最长匹配原则、行号列号的记录、错误字符的处理每一个都能让你调半天。这个实验适合谁如果你正在上编译原理课实验一是整个课程的地基后面语法分析、语义分析都要靠它输出的 token 流。如果你已经工作了想补一补编译前端的基础词法分析器也是最好的练手项目——它足够小一天能写完又足够完整能让你理解“自动机理论怎么落地成工程代码”。下面我就按“理论先立住、再动手能复现”的路子把这个实验拆开讲清楚。2. 词法分析的理论底座正则表达式、DFA 和最长匹配到底怎么对应2.1 从正则定义到 token 分类先画表再写代码词法分析器的理论根基是正则表达式和有限自动机。课本上会告诉你每种 token 都可以用正则表达式描述比如标识符是letter (letter | digit)*整数常量是digit digit*浮点数是digit . digit。但直接拿正则表达式去写代码是不现实的你需要先把所有 token 的正则定义整理成一张表明确优先级和识别规则。我一般会先做一张 token 规格表类似这样token 类型正则定义优先级示例关键字int/float/if/else/while/return最高int标识符letter (letter | digit)*高count、_tmp浮点常量digit . digit中3.14整数常量digit中42运算符-*/等低界符(){};,低;注释//到行尾 或/* ... */跳过// hello空白空格、制表、换行跳过\n这张表的关键在于优先级。为什么关键字要排在标识符前面因为int既符合关键字的正则也符合标识符的正则。如果你先匹配标识符int就会被当成普通标识符后面语法分析就全乱了。这就是词法分析里最经典的最长匹配和优先级问题当多个规则都能匹配时选最长的那一个长度相同时选优先级最高的那一个。提示很多同学实验一翻车不是代码写不出来而是这张表没理清楚。先把表画出来后面写代码就是翻译工作。2.2 DFA 的构造从 NFA 到状态转移表正则表达式可以直接转成 NFA非确定有限自动机再通过子集构造法转成 DFA确定有限自动机。理论课上你手算过但实验里你不需要真的在代码里建 NFA 再转 DFA——那是实验二或实验三可能做的事。实验一通常允许你直接手写 DFA 的状态转移或者用简单的switch-case模拟。我一般会先把所有 token 的 DFA 合并成一张状态转移表。举个例子识别标识符和关键字的 DFA 大致是状态 0初始状态读入字母则转到状态 1读入数字转到状态 2读入其他符号转到对应状态。状态 1已经读入一个或多个字母/数字继续读字母/数字则留在状态 1读其他字符则接受回退一个字符输出标识符或关键字。状态 2已经读入一个或多个数字继续读数字留在状态 2读小数点转到状态 3读其他字符则接受输出整数。状态 3已经读入数字和小数点继续读数字留在状态 3读其他字符则接受输出浮点数。这张表可以用二维数组表示也可以用if-else链。关键是要处理好回退当你多读了一个字符才发现当前 token 结束时需要把这个字符“退回去”留给下一个 token。很多同学在这里踩坑读着读着就把字符吞了导致下一个 token 识别错误。注意回退操作在代码里通常用ungetc或者自己维护一个缓冲区索引来实现。如果你用 Python可以用seek回退文件指针如果用 Cungetc只能回退一个字符要小心。2.3 手写词法分析器 vs 用 Lex/Flex实验一该怎么选理论上你可以用 Lex/Flex 自动生成词法分析器但湖南大学这个实验一通常要求手写。为什么因为手写才能让你真正理解 DFA 的运行过程。Flex 生成的代码你看不到状态转移的细节调 bug 的时候就是黑匣子。手写词法分析器的结构一般是这样// 伪代码结构 Token getNextToken() { skipWhitespaceAndComments(); if (isEOF()) return EOF_TOKEN; char ch peek(); if (isLetter(ch)) { return readIdentifierOrKeyword(); } else if (isDigit(ch)) { return readNumber(); } else if (isOperator(ch)) { return readOperator(); } else { return makeErrorToken(ch); } }这个结构清晰、好调试而且每一步你都能打印出来看。我建议实验一就用这种“大循环 分支”的写法不要一上来就搞状态机表驱动除非你已经很熟。选型理由很简单实验一的目的是让你理解词法分析的原理不是让你炫技。手写代码虽然看起来笨但每一个字符的读取、每一个状态的跳转都在你眼皮底下出了错你能立刻定位。等你把实验一做完再去看 Flex 的生成代码会有一种“原来它帮我做了这些”的顿悟。3. 动手实现用 C/C 或 Python 把词法分析器跑通3.1 环境准备与输入输出约定湖南大学实验一通常会给一个输入文件里面是一段类似 C 语言的源代码。输出要求一般是每行一个 token格式类似类型, 值, 行号或者类型, 值。具体格式看当年的实验指导书但核心字段就这几个。我一般用 C 或 C 写因为课本例子多是 C 风格而且指针操作和字符处理比较直接。如果你更熟 Python也完全没问题Python 的字符串处理反而更省心。下面我以 C 为例给出关键代码骨架。先定义 token 类型和结构// token.h typedef enum { TOKEN_KEYWORD, TOKEN_IDENTIFIER, TOKEN_INT_CONST, TOKEN_FLOAT_CONST, TOKEN_OPERATOR, TOKEN_DELIMITER, TOKEN_EOF, TOKEN_ERROR } TokenType; typedef struct { TokenType type; char value[256]; int line; int column; } Token;这里value存 token 的原始字符串line和column用于报错定位。很多同学实验一不记录行号后面语法分析报错时找不到位置血泪经验。输入输出约定要提前确认输入是文件还是标准输入输出是打印到屏幕还是写到文件token 类型名用英文还是中文这些细节实验指导书里都有别自己猜。3.2 核心扫描循环跳过空白、识别标识符和关键字核心扫描循环是词法分析器的心脏。我一般写成这样// lexer.c #include stdio.h #include ctype.h #include string.h static FILE *src; static int line 1; static int column 0; int peek() { int ch fgetc(src); if (ch ! EOF) ungetc(ch, src); return ch; } int advance() { int ch fgetc(src); if (ch \n) { line; column 0; } else if (ch ! EOF) { column; } return ch; } void skipWhitespaceAndComments() { int ch; while ((ch peek()) ! EOF) { if (isspace(ch)) { advance(); } else if (ch /) { advance(); int next peek(); if (next /) { // 单行注释跳到行尾 while ((ch advance()) ! EOF ch ! \n); } else if (next *) { // 块注释跳到 */ advance(); // 吃掉 * while ((ch advance()) ! EOF) { if (ch * peek() /) { advance(); // 吃掉 / break; } } } else { // 不是注释回退 ungetc(/, src); break; } } else { break; } } } Token readIdentifierOrKeyword() { Token tok; tok.line line; tok.column column; int idx 0; int ch; while ((ch peek()) ! EOF (isalnum(ch) || ch _)) { tok.value[idx] advance(); } tok.value[idx] \0; // 查关键字表 if (isKeyword(tok.value)) { tok.type TOKEN_KEYWORD; } else { tok.type TOKEN_IDENTIFIER; } return tok; }这段代码的逻辑说明peek()看下一个字符但不消耗advance()消耗一个字符并更新行列号。skipWhitespaceAndComments()负责跳过空白和注释注意块注释的*/匹配要小心别漏了。readIdentifierOrKeyword()循环读取字母、数字、下划线然后查关键字表决定是关键字还是标识符。参数说明line和column是全局状态每次advance()时更新。tok.value用固定大小数组实际项目中建议用动态字符串但实验一够用了。提示关键字表可以用一个字符串数组加线性查找也可以用哈希表。实验一的关键字就十几个线性查找完全够。3.3 数字常量与运算符的识别最长匹配和回退处理数字常量的识别要区分整数和浮点数核心是遇到小数点时继续读遇到其他字符时回退。代码大致这样Token readNumber() { Token tok; tok.line line; tok.column column; int idx 0; int ch; int hasDot 0; while ((ch peek()) ! EOF) { if (isdigit(ch)) { tok.value[idx] advance(); } else if (ch . !hasDot) { hasDot 1; tok.value[idx] advance(); } else { break; } } tok.value[idx] \0; tok.type hasDot ? TOKEN_FLOAT_CONST : TOKEN_INT_CONST; return tok; }运算符的识别要注意双字符运算符比如、!、、、、||。这些需要向前看一个字符Token readOperator() { Token tok; tok.line line; tok.column column; int ch advance(); tok.value[0] ch; tok.value[1] \0; int next peek(); if ((ch next ) || (ch ! next ) || (ch next ) || (ch next ) || (ch next ) || (ch | next |)) { tok.value[1] advance(); tok.value[2] \0; } tok.type TOKEN_OPERATOR; return tok; }这里的逻辑说明先读一个字符然后看下一个字符是否能组成双字符运算符。如果能就再读一个否则就单字符运算符。这就是最长匹配原则的体现。参数说明tok.value要预留足够空间双字符运算符占两个字符加结束符。peek()在这里很关键它让你不用回退就能判断下一个字符。注意和|单独出现时可能是位运算符也可能是错误。实验一通常只要求识别和||单个或|可以报错或当普通运算符处理看实验要求。3.4 主函数与测试用例用一段小程序验证输出主函数很简单循环调用getNextToken()直到 EOFint main(int argc, char *argv[]) { if (argc 2) { fprintf(stderr, Usage: %s source_file\n, argv[0]); return 1; } src fopen(argv[1], r); if (!src) { perror(fopen); return 1; } Token tok; do { tok getNextToken(); printToken(tok); } while (tok.type ! TOKEN_EOF); fclose(src); return 0; }测试用例建议用一段包含所有 token 类型的小程序int main() { int count 42; float pi 3.14; // 这是注释 if (count 10 pi ! 0.0) { count count 1; } return 0; }跑一遍看看输出是否包含关键字int、float、if、return标识符main、count、pi整数42、10、1、0浮点数3.14、0.0运算符、、、!、界符(、)、{、}、;。如果少了哪个就回去查对应的识别逻辑。提示测试用例要覆盖边界情况比如intx应该被识别为标识符而不是关键字int加标识符x3.和.14这种不完整的数字要报错还是按规则处理看实验要求。4. 避坑与排查词法分析器最容易翻车的 5 个地方4.1 现象关键字被识别成标识符 → 原因匹配顺序错了 → 解决先查关键字表这是最经典的翻车。你写int count;输出里int的类型是IDENTIFIER而不是KEYWORD。原因很简单你在readIdentifierOrKeyword()里先返回了TOKEN_IDENTIFIER忘了查关键字表。解决方法是读完字符串后立刻查关键字表命中就返回TOKEN_KEYWORD否则返回TOKEN_IDENTIFIER。关键字表要包含所有语言保留字别漏了while、else、return这些。4.2 现象数字后面多读了字符 → 原因回退没做或做错了 → 解决用 peek 代替 advance比如输入421你的输出是整数42或者整数421。原因是你用advance()读数字读到时已经消耗了它但没有回退。解决方法是用peek()先看确认是数字再advance()或者用ungetc回退。我一般推荐peek()方案逻辑更清晰不容易出错。4.3 现象块注释没跳过导致后续 token 全乱 → 原因*/匹配逻辑有漏洞 → 解决逐字符扫描并检查两个字符块注释/* ... */的结束标志是两个字符*和/。很多同学写成读到*就结束结果/* abc * def */这种中间有*的注释就提前结束了。正确做法是读到*时再看下一个字符是不是/是才结束不是就继续。代码里用peek()判断别用advance()两次。4.4 现象行号列号不对 → 原因换行时没重置列号 → 解决在 advance 里统一维护行号和列号是报错定位的关键。常见错误是换行时只增加了行号忘了把列号重置为 0或者peek()也增加了列号导致列号偏大。解决方法只在advance()里更新行列号peek()不动。换行时linecolumn 0其他字符column。4.5 现象程序在文件末尾崩溃或死循环 → 原因EOF 处理不对 → 解决所有循环都要检查 EOF文件末尾是最容易出 bug 的地方。peek()返回 EOF 时你的循环如果没检查就会一直读一直读死循环。或者advance()返回 EOF 后你还继续用这个值导致越界。解决方法所有while循环都要有ch ! EOF的条件getNextToken()在文件末尾返回TOKEN_EOF主循环收到TOKEN_EOF就退出。5. 进阶技巧用 Python 快速验证 DFA 逻辑再移植到 C如果你觉得 C 写起来太繁琐我推荐一个实用技巧先用 Python 把 DFA 逻辑跑通再移植到 C。Python 的字符串处理和调试输出更方便你可以快速验证状态转移是否正确然后再用 C 重写一遍。这样既保证了逻辑正确又满足了实验要求。具体做法用 Python 写一个Lexer类把每个 token 的识别写成一个方法用assert写测试用例。比如class Lexer: def __init__(self, text): self.text text self.pos 0 self.line 1 self.column 0 def peek(self): if self.pos len(self.text): return self.text[self.pos] return None def advance(self): ch self.text[self.pos] self.pos 1 if ch \n: self.line 1 self.column 0 else: self.column 1 return ch def read_identifier(self): start self.pos while self.peek() and (self.peek().isalnum() or self.peek() _): self.advance() value self.text[start:self.pos] if value in KEYWORDS: return (KEYWORD, value) return (IDENTIFIER, value)跑通之后把逻辑逐行翻译成 C。你会发现 C 的指针操作和 Python 的索引操作其实一一对应移植起来很快。而且 Python 里你可以随时print中间状态调 bug 效率高很多。验证方法写一个test_lexer.py用unittest或简单的assert检查每个 token 的类型和值。比如def test_keywords(): lexer Lexer(int count 42;) tokens lexer.tokenize() assert tokens[0] (KEYWORD, int) assert tokens[1] (IDENTIFIER, count) assert tokens[2] (OPERATOR, ) assert tokens[3] (INT_CONST, 42) assert tokens[4] (DELIMITER, ;)这些测试用例跑通再移植到 C基本不会出大问题。我当年就是靠这个办法一晚上把实验一写完第二天帮三个同学调 bug。希望帮到你。本文还有配套的精品资源点击获取