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

资讯详情

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

C++词法分析器完整实现:Qt界面+源码详解,编译原理课设可用

C++词法分析器完整实现:Qt界面+源码详解,编译原理课设可用 简介一份面向编译原理课程学习与教学演示的Java实践源码包专注于词法分析阶段基于SWING库实现可视化操作界面可在MyEclipse中导入运行适合计算机专业学生、教师及对编译器实现感兴趣的开发者。压缩包共收录5个Java源文件整体仅5KB代码量虽小但结构完整覆盖输入源程序读取、关键字与标识符等token模式定义、词法切分逻辑及错误提示等核心环节并配有详细注释便于逐段对照学习。目前已有524人学习下载曾用于编译原理课程设计与实验环节教学反馈较为直观。通过观察界面中词法分析的过程读者可以清晰理解正则表达式如何映射到有限状态自动机进而掌握从字符流到token流生成的编译器前端基础同时也能学习Java Swing事件驱动编程与MyEclipse工程的组织方式。整体轻量但功能点齐全是理论联系实际的入门级参考资料也可作为二次开发或课程报告的起点。 很多人把编译原理课设当成一个硬骨头尤其词法分析这一块理论书上的状态转换图画得明明白白真到自己动手写代码却总卡在“字符串怎么切”“注释怎么跳”“界面怎么和逻辑接上”这些具体问题上。这次分享一个我整理过的C实现词法分析器完整方案带Qt图形界面源码可以直接跑起来关键位置都写了注释。它能把一段C语言风格的源码自动拆成关键字、标识符、常数、运算符、界符还能标记非法字符和错误位置非常适合做课程设计参考或者给刚学编译原理的人当练手项目。这个项目不追求复杂的语法分析只把词法分析这一层做扎实支持整数、小数、科学计数法识别支持字符串和字符常量能跳过单行注释和块注释能区分关键字和普通标识符所有的Token都会带行号、列号、类别展示在界面表格里。核心词法逻辑和界面代码分离方便你改造升级比如接上符号表、改成语法分析的前端输入都很容易扩展。1. 整体设计与思路拆解1.1 词法分析到底在干什么词法分析是编译过程的第一站作用是把源代码的字符流转换成有意义的单词序列也就是Token。你可以把它理解为给一段英文做分词和词性标注看见int知道是关键字看见count知道是标识符看见3.14知道是常数看见知道是运算符。编译器后续的语法分析不用再关心字符层面的细节只消费Token流就行。做课程设计时最容易犯的错是拿到需求就写代码结果把词法规则散落在界面控件的响应函数里改一个界面bug能把识别逻辑也带崩。这个项目一开始就定了原则词法分析核心做成独立类界面只负责调用和展示两者之间用Token列表对接。这样核心逻辑可以在命令行环境下单独测试界面出错时也容易定位。1.2 为什么选C和Qt这套组合课程设计的语言选择通常有几种C/C、Java、Python。我选C配Qt主要从三个角度考虑。一是性能上处理大文件不费劲测试几百行的示例代码根本没压力二是Qt的QString对中文和Unicode处理很友好写界面方便QTableWidget、QTextEdit这些控件直接摆出来就有完整交互三是Qt跨平台在Windows和Linux上都能编译运行交作业或者自己留着扩展都合适。如果你熟悉其他语言核心思路完全可以平移。比如用Java就写一个Lexer类配Swing或JavaFX界面用Python就写个正则扫一遍配Tkinter或PyQt。但C版的好处是对编译原理理解更底层状态机的过程写得更清楚不像正则解决一切那样黑盒。1.3 模块划分界面和逻辑彻底分离整个项目分成三层。第一层是Token数据结构定义类别、值、行号、列号第二层是Lexer核心类接收整个源码字符串输出Token列表和错误列表第三层是MainWindow界面类负责打开文件、读取编辑框内容、调用Lexer、把结果显示到表格和错误栏。界面代码里不出现任何一个字符识别的细节按钮点击事件只做数据交换。这样做还有一个额外好处单元测试好写。你可以直接构造一个字符串丢给Lexer检查返回的Token数量和类型是否正确不用每次启动图形界面去点按钮验证。我当时就是先用控制台把核心类测通再套界面整个开发过程省了很多来回试错的力气。2. 核心功能拆解从字符流到Token序列2.1 关键字表和符号表怎么定词法分析先要定义“语言”的单词分类。课程设计一般用C语言的子集就够了不需要支持全部语法。这个项目定义了五类Token关键字、标识符、常数、运算符和界符、字符串与字符常量。关键字集合用的是C语言常用部分包括int、float、double、char、void、if、else、while、for、return、break、continue、const、struct、typedef。识别到标识符后先查关键字表命中就标记为关键字否则是普通标识符这个查表法最简单也最稳。运算符和界符则覆盖 - * / % ! || ! | ( ) { } [ ] ; ,这些常见符号。注意两个细节一是像、这种双字符运算符必须先看下一个字符再决定是单符号还是双符号二是注释符//和/*要提前拦截否则/会被当成除号处理。Token类别示例识别方式关键字int, if, while查预定义关键字表标识符count, _temp字母或下划线开头后续字母数字下划线整常数123, 0x1F十进制数字序列支持十六进制前缀浮点常数3.14, 1e-5小数点或指数符号触发浮点分支运算符, , 单字符或双字符匹配界符;, {, }, ( )单字符匹配字符串/字符hello, a引号包裹支持转义符注释// x, /* x */识别后直接跳过不生成Token2.2 数字、标识符和字符串的状态识别这部分是词法分析最核心的手工逻辑。项目的Lexer采用逐字符扫描的方式用一个for循环配合游标位置判断遇到不同起始字符就进入不同的识别分支。这里单独说一下数字识别的处理因为它是新手踩坑重灾区。整数的识别比较直接看到数字字符就继续向后读直到遇到非数字字符。但要区分十六进制就得在看0x时特判0x后面必须跟十六进制字符否则报错。浮点数的情况更多一旦读数字过程中遇到小数点.就要标记进入小数状态遇到e或E则进入指数状态。最tricky的地方在于123abc这种非法串不能只读123就结束而应该一直读到非字母数字符号为止然后把整段报告为非法Token让用户知道源码哪里写错了。字符串和字符常量的处理也是类似遇到或后一直读直到匹配的引号出现如果读到换行还没结束就报告“字符串未闭合”。注意转义字符\n、\\、\要特殊处理跳过一个字符后再继续否则遇到2\这种内容会提前误判字符串结束。我写代码时为此单独处理了当前字符后面是不是反斜杠避免转义序列打断字符串扫描。// 数字识别核心示意实际代码会拆成几个私有方法 if (ch.isDigit()) { int start i; bool isFloat false; // 支持十六进制0x或0X开头 if (ch 0 (i 1 size) (text.at(i 1) x || text.at(i 1) X)) { i 2; while (i size (text.at(i).isDigit() || (text.at(i).toLower() a text.at(i).toLower() f))) i; } else { while (i size text.at(i).isDigit()) i; // 小数点后必须跟数字才行 if (i 1 size text.at(i) . text.at(i 1).isDigit()) { isFloat true; i; while (i size text.at(i).isDigit()) i; } // 指数部分 if (i 1 size (text.at(i) e || text.at(i) E)) { int temp i 1; if (temp size (text.at(temp) || text.at(temp) -)) temp; if (temp size text.at(temp).isDigit()) { isFloat true; i temp; while (i size text.at(i).isDigit()) i; } } } QString number text.mid(start, i - start); emitToken(isFloat ? 浮点常数 : 整数常数, number, line, col); i--; }2.3 注释、空白符和行号列号维护空白字符包括空格、制表符、换行符识别时直接跳过不生成Token。但换行符的位置必须记录好因为每个Token都需要行号列号后续做错误定位才准确。我维护了line和col两个变量每遇到\n就把行号加1、列号归零。注释处理有两个分支//单行注释直接跳到行尾/*块注释则一直找到*/为止如果到文件末尾都没找到就报告“块注释未闭合”。这里有个细节是块注释跨行时中间可能包含换行符所以注释跳过的循环里也要同步更新行号和列号不能用简单粗暴的text.indexOf(*/)不加判断。识别完注释后继续循环相当于注释被完全透明化。注意注释内部的字符不做任何词法识别这是词法分析的一个重要特性。// http://example.com里的//之后全部被跳过不会误识别成冒号和斜杠。2.4 错误处理和非法字符上报词法分析器必须能对源码中的非法内容给出明确反馈。项目里把错误也设计成一种Token类别叫“错误”值和具体描述一起放进错误列表。遇到实在无法归类的字符比如、$、中文全角符号就记录错误Token报“非法字符”并标明行列号界面端会把错误行用红色显示。识别错误Token的方式是兜底逻辑所有合法分类都判断完之后剩余字符一律按非法处理。这里需要注意一点很多教材只讲了合法Token的识别忽略了错误恢复。词法分析过程中遇到错误不能直接退出否则后面正确代码也分析不了。正确处理方式是把错误记录下来跳过这个非法字符继续扫描让用户一次性看到全部问题。3. 界面设计让词法分析过程看得见3.1 界面布局和交互流程界面采用左右分栏布局。左侧是一个QTextEdit作为源码编辑区用户可以直接输入代码也可以点击“打开文件”读取本地.c文件。右侧是一个QTableWidget展示Token表格表格有五列序号、单词、类别、行号、列号。下方还有一块QTextEdit作为错误输出区分析结果里的错误信息都会汇总到这里。顶部放一排按钮“打开文件”“开始分析”“清空结果”。交互流程很简单先编辑源码或打开文件点击“开始分析”程序把左侧源码字符串传给Lexer得到Token列表后逐行填入表格语法高亮部分我在3.3小节单独讲。整个过程不超过300毫秒界面响应很快大文件也能轻松处理。3.2 信号槽连接和数据显示Qt的信号槽机制让界面和逻辑的对接非常直观。“开始分析”按钮的clicked信号连接到MainWindow::onAnalyze槽函数。槽函数里通过sourceEdit-toPlainText()获取输入构造Lexer对象调用analyze()方法把结果写到表格和错误栏。void MainWindow::onAnalyze() { QString code sourceEdit-toPlainText(); Lexer lexer(code); QListToken tokens lexer.analyze(); tokenTable-setRowCount(tokens.size()); tokenTable-setColumnCount(5); tokenTable-setHorizontalHeaderLabels({序号, 单词, 类别, 行号, 列号}); for (int i 0; i tokens.size(); i) { const Token tok tokens[i]; QTableWidgetItem *itemType new QTableWidgetItem(tok.type); if (tok.type 错误) { itemType-setBackground(QBrush(QColor(#ffcccc))); } tokenTable-setItem(i, 0, new QTableWidgetItem(QString::number(i 1))); tokenTable-setItem(i, 1, new QTableWidgetItem(tok.value)); tokenTable-setItem(i, 2, itemType); tokenTable-setItem(i, 3, new QTableWidgetItem(QString::number(tok.line))); tokenTable-setItem(i, 4, new QTableWidgetItem(QString::number(tok.column))); } }说一个实际开发中遇见的坑QTableWidget默认双击可编辑这在展示结果时会误触发修改影响体验。用户体验优化上我给表格设置了setEditTriggers(QAbstractItemView::NoEditTriggers)保证分析结果只能看不能改源码编辑区才是唯一可编辑区域。3.3 进阶界面扩展源码高亮和选中联动基础的表格展示做完后项目还扩展了一个让演示效果提升一截的功能源码区根据Token类别做高亮。识别出关键字后给源码中对应位置的字词着蓝色标识符保持黑色常数用深红色运算符和界符用深灰色。这个功能只要在analyze结束后遍历Token列表用QTextCursor选中对应的源码区间设置字符颜色即可。我建议你再加一个表格选中行联动源码高亮定位用户点击Token表格某一行时currentCellChanged信号触发源码区自动滚动到对应Token的行并用背景色高亮那一小段。这个功能对教学演示特别有用老师能直观看到源码的哪个片段对应哪个Token答辩时是加分项。代码量不大核心就是根据行列号计算位置再设置光标选区。4. 完整运行流程和常见问题排查4.1 环境准备和编译运行项目用Qt 5.15或Qt 6.x都可以我用了Qt 5.15稳定版编译器选了MinGW 64位。创建工程有两种方式直接新建Qt Widgets Application把Lexer.h、Lexer.cpp、MainWindow.h、MainWindow.cpp这些文件加进工程或手写一个.pro文件内容很简单核心就是声明QT widgets和添加源文件头文件。构建步骤用Qt Creator打开.pro文件选择构建套件Desktop Qt 5.15.2 MinGW 64-bit点构建运行。如果一切正常Qt Creator会自动弹出界面窗口。需要注意项目的构建目录里不要有中文路径否则偶尔会出现编译阶段找不到文件或运行时动态库加载失败的怪问题。QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET LexerUI TEMPLATE app SOURCES \ main.cpp \ MainWindow.cpp \ Lexer.cpp HEADERS \ MainWindow.h \ Lexer.h在命令行单独测试核心逻辑时我在main.cpp里保留了一段注释掉的测试代码直接用QCoreApplication构造一个源码字符串调用Lexer::analyze并打印每个Token方便在不开界面的情况下验证词法规则。这个习惯建议保留后续改代码可以先命令行验证再打开界面看效果。4.2 测试用例演示我准备了一段测试代码覆盖了大部分词法规则关键字、变量名、整数、浮点数、科学计数法、运算符、注释、字符串、非法字符。把这段代码粘进界面左侧编辑区点击“开始分析”右侧表格会生成对应Token序列错误栏会提示符号和第8行末尾的中文全角分号是非法字符。/* 示例程序计算两个数的和 */ #include stdio.h int add(int a, int b) { double rate 3.14e2; // 科学计数法 float value 2.5f; char mode A; const char* msg hello; return a b; } int invalid_var 123abc;这个例子基本能把本项目支持的全部Token类别都覆盖到。123abc会作为一个整体被识别成非法Token这不是bug而是设计上故意这么处理的编译器源码里这种错误必须被完整标记出来不拆开丢信息。4.3 高频问题排查速查整理一下我做这个项目时踩过的坑以及同学们经常咨询的问题按频率和影响程度排个序。问题现象可能原因解决办法中文注释显示乱码源文件编码不是UTF-8统一用UTF-8编码保存Qt中读取文件用QString::fromUtf8打开文件没有内容文件路径写错或用了相对路径先转为绝对路径用QFileInfo(filePath).absoluteFilePath()检查多个连续注释跨行时行号错乱块注释内部换行没有更新行号在注释跳过的while循环里同步处理\n数字后面接字母不被识别为错误识别数字在遇到字母时立即结束数字后如果接着字母或下划线继续读完整段整段标记错误字符串内转义引号导致提前结束没有处理反斜杠跳转遇到\\或\时跳过下一个字符再继续被识别成两个多字符运算符没有特判先看下一个字符合并成双字符运算符再返回补充一个调试技巧如果你发现某个Token的类型或边界不对可以在Lexer类里临时加一个qDebug()打印当前位置字符和状态逐字追踪扫描过程。这比在界面里一步步点要高效得多我排查块注释误判问题的时候就是用这个方法三分钟定位到了换行更新位置漏了一处。5. 项目扩展方向和几条实在建议5.1 从词法分析到编译器前端现在的词法分析器已经能把源码拆成Token流下一个自然的扩展是接上语法分析。你可以在Token列表基础上做一个递归下降解析器判断if后面是否跟了括号、while的循环体是否配对这其实就是编译原理课程的下一个阶段。建议把Token列表设计成可序列化的结构语法分析模块直接消费不需要再改词法部分。另一个常见的扩展是加符号表。在识别标识符的同时把函数名、变量名登记到符号表里记录作用域和类型信息。符号表可以做成独立的SymbolTable类用一个QMapQString, SymbolInfo存名字和属性词法分析阶段先做简单登记语法分析阶段再丰富类型和值域。这样项目深度一下就从课程设计拉到了小型编译器前端的水准。5.2 我实践下来最重要的三件事第一件核心逻辑和界面分离。不是所有代码都能做到但凡是能运行、能测试的核心模块都值得单独剥出来。我把Lexer类完全独立之后界面上所有的改动都再也没影响过词法规则省了太多联调时间。第二件先命令行测试再套界面。很多同学喜欢一开始就把界面搭好一边点按钮一边改代码效率很低。正确的做法是在main函数里直接构造源码字符串去跑Lexer用控制台输出验证每一条规则界面只用来看最终效果。第三件处理任何字符都要记得维护行号和列号。我一开始把行号列的更新分散在各个分支里后来发现注释跳过和字符串处理会漏掉换行干脆把所有字符遍历统一到一个函数入口每次读到字符都先更新行列号再进入状态分支从此再没出过错。这个项目如果只是交作业上面的内容完全够用。如果你想继续往深钻研可以尝试在词法分析过程中加入位置信息重定位或者做一个状态转换图的实时可视化把教材里的图动态画出来。编译原理没有想象中那么难把它拆成一个一个小的状态判断一步一步处理最终跑出正确结果的时候那种成就感还是很踏实的。本文还有配套的精品资源点击获取
返回列表