news 2026/10/2 1:01:36

湖南大学编译原理实验一:手写词法分析器从理论到代码完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
湖南大学编译原理实验一:手写词法分析器从理论到代码完整指南

简介:这份资源是湖南大学编译原理课程实验一的配套资料包,面向正在修读该课程、需要完成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回退文件指针;如果用 C,ungetc只能回退一个字符,要小心。

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()直到 EOF:

int 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加标识符x,3.和.14这种不完整的数字要报错还是按规则处理,看实验要求。

4. 避坑与排查:词法分析器最容易翻车的 5 个地方

4.1 现象:关键字被识别成标识符 → 原因:匹配顺序错了 → 解决:先查关键字表

这是最经典的翻车。你写int count;,输出里int的类型是IDENTIFIER而不是KEYWORD。原因很简单:你在readIdentifierOrKeyword()里先返回了TOKEN_IDENTIFIER,忘了查关键字表。解决方法是读完字符串后立刻查关键字表,命中就返回TOKEN_KEYWORD,否则返回TOKEN_IDENTIFIER。关键字表要包含所有语言保留字,别漏了while、else、return这些。

4.2 现象:数字后面多读了字符 → 原因:回退没做或做错了 → 解决:用 peek 代替 advance

比如输入42+1,你的输出是整数42+或者整数421。原因是你用advance()读数字,读到+时已经消耗了它,但没有回退。解决方法是用peek()先看,确认是数字再advance();或者用ungetc回退。我一般推荐peek()方案,逻辑更清晰,不容易出错。

4.3 现象:块注释没跳过导致后续 token 全乱 → 原因:*/匹配逻辑有漏洞 → 解决:逐字符扫描并检查两个字符

块注释/* ... */的结束标志是两个字符*和/。很多同学写成读到*就结束,结果/* abc * def */这种中间有*的注释就提前结束了。正确做法是:读到*时再看下一个字符是不是/,是才结束;不是就继续。代码里用peek()判断,别用advance()两次。

4.4 现象:行号列号不对 → 原因:换行时没重置列号 → 解决:在 advance 里统一维护

行号和列号是报错定位的关键。常见错误是换行时只增加了行号,忘了把列号重置为 0;或者peek()也增加了列号,导致列号偏大。解决方法:只在advance()里更新行列号,peek()不动。换行时line++,column = 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。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 1:01:19

Zemax IMAE多模光纤耦合效率优化五步法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 0:47:46

Unity3D展馆系统开发:C#驱动的机场数字孪生交互实践

1. 项目概述&#xff1a;这不是一个“飞机场模拟器”&#xff0c;而是一套面向公众教育与行业展示的三维交互式展馆系统“基于Unity3DC#实现的飞机场漫游展馆系统”——这个标题里藏着三个关键信号&#xff1a;Unity是引擎底座&#xff0c;3D是空间载体&#xff0c;C#是逻辑中枢…

作者头像 李华
网站建设 2026/10/2 0:43:42

多模型API集成实战:DeepSeek、Qwen、GLM统一工作台搭建指南

1. 为什么要把三个模型塞进同一个工作台先说结论&#xff1a;把 DeepSeek、Qwen、GLM 放进同一个工作台&#xff0c;本质上不是为了"集邮"&#xff0c;而是为了解决一个非常具体的痛点——不同任务对模型的能力需求差异极大&#xff0c;而频繁切换网页端或客户端会严…

作者头像 李华
网站建设 2026/10/2 0:35:38

SQL Server网络协议配置与连接排查:从Shared Memory到TCP/IP

刚装完 SQL Server&#xff0c;很多人的第一反应是拿 SSMS 在本机敲个“.”就连上了&#xff0c;感觉一切顺利。等到换一台电脑&#xff0c;或者让某个第三方应用去连数据库&#xff0c;就开始各种报错&#xff1a;找不到服务器、无法建立连接、证书链有问题……这时候十有八九…

作者头像 李华
网站建设 2026/10/2 0:33:39

SSM+JSP+MySQL共享汽车租赁平台项目实战解析

开头做这个共享汽车租赁平台之前&#xff0c;我带过一个毕业生团队做过类似的项目&#xff0c;但自己真正从零把一个基于javaweb和mysql的ssm共享汽车租赁平台跑通&#xff0c;还是在接手这个项目之后。整套技术栈是javassmjspjquerymysql&#xff0c;Spring管业务对象、Spring…

作者头像 李华
网站建设 2026/10/2 0:26:52

智能家居硬件开源项目去哪找?4类资源渠道与实操学习路径

做智能家居硬件开发这几年&#xff0c;我几乎每周都会在交流群里看到有人问同一个问题&#xff1a;到底去哪里找智能家居硬件开源项目&#xff1f;市面上的教程零零散散&#xff0c;有的只有一个 Demo 视频&#xff0c;有的仓库躺在收藏夹里半年没动过&#xff0c;真正能跑起来…

作者头像 李华