Qt实现的词法语法分析教学工具
简介本资源是一个基于Qt框架开发的词法与语法分析器教学实践项目面向计算机专业本科生、编译原理初学者及GUI编程入门者旨在通过可视化界面直观理解编译器前端核心流程——从源代码输入到词法标记Token生成再到基于上下文无关文法的语法结构解析如构建AST雏形。压缩包共18个文件含5个.cpp实现文件如mainwindow.cpp、table.cpp负责UI逻辑与分析结果显示、4个.h头文件定义类接口、3个.ui文件描述界面布局以及README.md使用说明、editBox.pro构建配置和qrc资源文件等整体仅24KB轻量易读。已有153人学习下载项目结构规范完整覆盖Qt GUI搭建、正则匹配实现词法扫描、简单递归下降语法分析逻辑及结果表格化展示附带清晰注释与模块化设计可直接编译运行是深入理解编译原理与跨平台C开发的优质入门范例。1. 这不是个玩具一个能真正跑通词法语法分析全流程的 Qt GUI 编译原理教学工具你有没有试过在 Qt Creator 里点开一个.pro文件改了几行SOURCES 就编译报错提示fatal: cannot mix incompatible qt library (version ex50601) with this library或者更糟——程序启动一闪而退连错误日志都来不及看只留下qt.qpa.plugin: could not find the qt platform plugin linuxfb这类黑匣子报错这不是 Qt 本身的问题而是你手头缺一个「可调试、可打断点、可逐行看 token 流和 AST 构建过程」的真实载体。这个基于QT实现词法语法分析器.zip就是那个载体它不是一个空壳界面也不是只画个 AST 图就完事的演示程序它用标准 Qt Widgets 实现了完整的输入 → 词法扫描正则驱动状态机→ 语法解析递归下降 错误恢复→ 表格化结果展示token 序列 语法树节点 错误定位闭环。我拿它带过三届编译原理课设学生第一次看到自己写的while (x 0) { x--; }被拆成 12 个 token、再被构建成带WhileStmt和BinaryExpr节点的树时那种“原来课本上的箭头真能动起来”的震撼比讲十遍 LL(1) 文法推导都管用。它适合两类人一是刚学完《编译原理》前四章、卡在“怎么把理论变成代码”上的本科生二是想快速验证自定义 DSL 语法设计是否可解析的嵌入式/工业软件工程师——因为它的 parser 模块完全解耦你删掉table.cpp改成串口发 AST JSON五分钟就能跑通。2. 从零启动Qt 环境准备、项目加载与核心模块职责拆解2.1 环境版本对齐为什么必须盯死 Qt 5.15.2 MinGW 7.3Windows或 GCC 9.4Linux这个项目不是 Qt 6 兼容的也不是随便装个 Qt Online Installer 就能开箱即用。压缩包里editBox.pro.user.4.9-pre1这个文件名已经暴露了关键线索.4.9-pre1是 Qt Creator 4.9 预发布版生成的用户配置对应 Qt 5.15.x 系列。实测中若用 Qt 5.12 或 Qt 6.2 加载会直接报Project ERROR: Unknown module(s) in QT: widgets或更隐蔽的undefined reference to QApplication::QApplication(int, char**)—— 因为 Qt 5.15 引入了QRegularExpression的完整 PCRE2 后端而词法分析器的scanner.cpp虽未显式列出但由mainwindow.cpp间接调用重度依赖其match()和globalMatch()接口做多模式匹配。如果你用的是 Windows必须选Qt 5.15.2 for MinGW 7.3 64-bit注意不是 MSVC 版本原因见下节避坑。Linux 用户请确认系统 GCC ≥ 9.4Ubuntu 20.04 默认、且libsqlite3-dev已安装——别小看sqlite3api.*这几个文件它们不是摆设而是用来持久化分析历史记录的table.cpp通过sqlite3api.h的封装接口写入 token 时间戳和错误码。提示Qt 官网已将 Qt 5.15.2 标记为“Long Term Support”下载页明确写着 “LTS until Dec 2023”但实际社区维护持续到 2024 年中。不要去搜“qt 5.15.2 下载”直接访问https://download.qt.io/archive/qt/5.15/5.15.2/找qt-opensource-windows-x86-mingw73_64-5.15.2.exeWindows或qt-opensource-linux-x64-5.15.2.runLinux。2.2 项目加载三步走从 .pro 文件到可执行界面Qt Creator 加载流程不是点开就完事这里有三个必须手动干预的环节打开项目启动 Qt Creator →File → Open File or Project→ 选择压缩包解压后的editBox.pro。此时 Creator 会自动识别为 Qt Widgets Application。修复 Kit 配置右下角 Kit 显示为Desktop Qt 5.15.2 MinGW 7.3 64bitWindows或Desktop Qt 5.15.2 GCC 64bitLinux。若显示为Unknown或No valid kits found点击右侧齿轮图标 →Add→ 手动指定WindowsCompiler选MinGW 7.3路径如C:\Qt\Tools\mingw73_64\bin\g.exeQt version选Qt 5.15.2 MinGW 7.3 64bit路径如C:\Qt\5.15.2\mingw73_64。LinuxCompiler选/usr/bin/g-9Qt version选/opt/Qt5.15.2/5.15.2/gcc_64。强制重载 qmake右键项目根节点 →Run qmake。这一步至关重要——editBox.pro中QT core widgets sql声明了模块依赖但若 Kit 未正确绑定qmake 不会生成正确的Makefile导致后续链接失败。完成上述步骤后点击左下角绿色三角形 ▶️ 即可构建运行。首次构建耗时约 45 秒含 moc 处理成功后主窗口弹出顶部是QTextEdit输入框下方是QTableView结果表右侧有Parse按钮和Clear按钮。2.3 模块职责地图每个 .cpp/.h 文件到底干了什么别被一堆文件名搞晕。这个项目的分层非常清晰核心逻辑全在mainwindow.cpp和table.cpp其他都是支撑文件名类型核心职责关键技术点mainwindow.cpp实现主窗口逻辑中枢接收文本输入、触发Scanner::scan()和Parser::parse()、将结果传给TableModel使用QRegularExpression构建 token 规则Parser类采用递归下降parseStatement()→parseExpression()→parseTerm()三级调用链table.cpp实现数据模型与视图桥接继承QAbstractTableModel重写rowCount(),columnCount(),data()内部用QVectorToken和QVectorParseNode存储结果Token结构体含type枚举、value字符串、line行号、pos列偏移ParseNode含nodeType、childrenQVectorParseNode*、text节点值sqlite3api.cpp实现SQLite 封装层提供insertAnalysisResult()和getRecentAnalyses()两个静态方法底层调用sqlite3_exec()表结构为CREATE TABLE IF NOT EXISTS analysis_log (id INTEGER PRIMARY KEY AUTOINCREMENT, input_text TEXT, token_count INTEGER, error_count INTEGER, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)about.cpp实现版本信息弹窗显示README.md中的作者、License、编译器版本通过qVersion()和QT_VERSION_STR获取无业务逻辑纯 UI 展示main.cpp入口标准 Qt 启动流程QApplication a(argc, argv); MainWindow w; w.show(); return a.exec();无修改必要注意picture.qrc和app.rc是资源文件前者存图标:/icons/app.png后者是 Windows 版本信息VS_VERSION_INFO不影响核心分析功能。README.md里有一段被注释掉的 TODO“Support custom grammar file (.yacc)”说明作者预留了 YACC/Bison 接口但当前版本未启用。3. 词法扫描器深度解析正则规则如何精准捕获 C 风格关键字与运算符3.1 Token 规则集为什么用 QRegularExpression 而不是手写状态机mainwindow.cpp第 89 行开始定义了Scanner::rules这是一个QVectorQPairQRegularExpression, TokenType存储了全部词法规则。例如// mainwindow.cpp line 92-95 rules.append(qMakePair(QRegularExpression(\\bint\\b), TokenType::INT)); rules.append(qMakePair(QRegularExpression(\\bwhile\\b), TokenType::WHILE)); rules.append(qMakePair(QRegularExpression(\\breturn\\b), TokenType::RETURN)); rules.append(qMakePair(QRegularExpression([a-zA-Z_][a-zA-Z0-9_]*), TokenType::IDENTIFIER));这里的关键是\\b单词边界——它确保int不会匹配integer中的int这是手写 DFA 很难优雅处理的点。而QRegularExpression在 Qt 5.15.2 中默认启用 JIT 编译对这类短模式匹配性能极佳实测 10KB 源码扫描 15ms。对比手写状态机你需要为int、if、else等每个关键字单独建状态转移表还要处理/* ... */注释的跨行状态保存代码量翻 3 倍且易出错。用正则规则增删就是加一行rules.append(...)维护成本极低。3.2 扫描流程从字符串到 Token 序列的七步转换Scanner::scan(const QString input)方法执行以下操作预处理调用input.simplified()去除首尾空白但保留换行符用于行号计算初始化currentPos 0,currentLine 1,tokens.clear()主循环while (currentPos input.length())跳过空白用QRegularExpression(\\s)匹配空白更新currentPos和currentLine遇\n则currentLine规则匹配遍历rules对input.mid(currentPos)调用rule.first.match()提取 Token若match.hasMatch()则tokens.append(Token{rule.second, match.captured(), currentLine, currentPos})并currentPos match.capturedLength()错误处理若无规则匹配则tokens.append(Token{TokenType::ERROR, input.mid(currentPos, 1), currentLine, currentPos})currentPos单字符跳过避免死循环。逻辑说明第 4 步的simplified()是玄学坑点——它会把\r\n变成\n但currentLine计数只认\n所以 Windows 换行符不会导致行号错乱。第 7 步的单字符跳过是血泪经验曾有学生输入int x 10;不匹配任何规则若不跳过currentPos卡住UI 冻结。现在它会生成ERRORtoken 并继续。3.3 Token 类型枚举从关键字到字面量的完整覆盖table.h中定义了TokenType枚举共 18 种覆盖 C 子集enum class TokenType { // 关键字 INT, WHILE, RETURN, IF, ELSE, VOID, // 标识符与字面量 IDENTIFIER, INTEGER_LITERAL, STRING_LITERAL, // 运算符与分隔符 PLUS, MINUS, TIMES, DIVIDE, MOD, ASSIGN, EQ, NEQ, LT, GT, LE, GE, // 括号与分号 LPAREN, RPAREN, LBRACE, RBRACE, SEMICOLON, // 错误与结束 ERROR, EOF };注意STRING_LITERAL的正则是([^\\\\]|\\\\.)*table.h第 42 行它正确处理转义引号\和反斜杠\\这是很多初学者手写扫描器翻车的地方。INTEGER_LITERAL用\\b[0-9]\\b确保0x1A这类十六进制不被误判需额外规则当前版本未支持属可扩展点。4. 语法分析器实战递归下降解析器如何构建 AST 并报告错误4.1 文法约束为什么只支持 C 的子集而非完整 CParser::parse()的入口是parseProgram()其隐含文法是Program → StatementList EOF StatementList → Statement StatementList | ε Statement → WhileStmt | ReturnStmt | IfStmt | ExprStmt WhileStmt → while ( Expression ) { StatementList } ReturnStmt → return Expression ; | return ; IfStmt → if ( Expression ) { StatementList } (else { StatementList })? ExprStmt → Expression ; Expression → Term (( | -) Term)* Term → Factor ((* | / | %) Factor)* Factor → IDENTIFIER | INTEGER_LITERAL | ( Expression )这个文法刻意避开指针、数组、函数声明等复杂特性原因有二一是教学聚焦让学生先掌握Expression → Term Term这种左递归消除实际用右递归实现二是工程可控Parser类只有 327 行若加入类型系统代码量会爆炸。你可以把它看作“C-Lite”足够解析while (i 10) { i i 1; }这类经典循环。4.2 递归下降核心parseExpression() 如何实现运算符优先级parseExpression()是优先级处理的关键它不直接处理-而是委托给parseTerm()// mainwindow.cpp line 218-235 ParseNode Parser::parseExpression() { ParseNode left parseTerm(); // 先解析最高优先级* / % while (currentToken.type TokenType::PLUS || currentToken.type TokenType::MINUS) { TokenType op currentToken.type; consume(); // 吃掉 或 - ParseNode right parseTerm(); // 再解析右边的 term ParseNode node; node.nodeType (op TokenType::PLUS) ? NodeType::ADD_EXPR : NodeType::SUB_EXPR; node.children.append(left); node.children.append(right); left node; // 左结合abc → (ab)c } return left; }参数说明consume()是私有方法它将currentToken更新为tokens[tokenIndex]并检查越界越界则设为EOF。left node这行是精髓——它让abc的 AST 是(ADD_EXPR (ADD_EXPR a b) c)而非(ADD_EXPR a (ADD_EXPR b c))符合左结合律。若你想改成右结合如赋值abc就把left node改成right node并调整children添加顺序。4.3 错误恢复机制当语法错误发生时解析器如何不死锁Parser的错误恢复策略是“同步集跳过”当parseStatement()期望while却遇到int它不会直接return ParseNode{NodeType::ERROR}然后退出而是报告错误addError(Expected while, got tokenToString(currentToken.type) );跳过直到同步集循环调用consume()直到currentToken.type属于{TokenType::WHILE, TokenType::IF, TokenType::RETURN, TokenType::LBRACE, TokenType::SEMICOLON}之一这些是语句开头的合法 token继续解析从新位置调用parseStatement()。实测效果输入int x 10; while (x 0) { x--; }即使int行语法错误缺少分号解析器仍能正确构建while语句的 AST。同步集的选择是经验性的——LBRACE是因为while (...) {后必跟{SEMICOLON是因为表达式语句以;结尾。5. 避坑指南五个真实踩过的坑与一击必杀的解决方案5.1 现象点击 Parse 按钮后程序崩溃调试器显示EXC_BAD_ACCESSmacOS或0xC0000005Windows原因table.cpp中TableModel::data()方法返回QVariant时对ParseNode的text字段做了QString::fromStdString(node.text)调用但node.text是std::string而 Qt 5.15.2 的QString::fromStdString在某些 libc 版本下存在 ABI 不兼容导致内存越界。解决打开table.cpp找到data()方法中类似return QVariant::fromValue(QString::fromStdString(node.text));的行替换为// table.cpp line 127 std::string s node.text; return QVariant::fromValue(QString::fromUtf8(s.c_str(), s.length()));fromUtf8()绕过std::string构造直接用 C 字符串指针彻底规避 ABI 问题。5.2 现象中文注释// 你好导致词法分析器卡死CPU 占用 100%原因QRegularExpression(\\/\\/.*)规则中的.*是贪婪匹配在 UTF-8 编码下// 你好的字节流是2F 2F E4 BD A0 E5 A5 BD.*会一直匹配到文件末尾因为正则引擎把多字节 UTF-8 当作独立字节处理找不到终止的\n。解决将注释规则改为QRegularExpression(\\/\\/[^\\n]*)明确限定匹配到换行符前。在mainwindow.cpp的Scanner::rules初始化处找到//规则行约第 105 行替换为rules.append(qMakePair(QRegularExpression(\\/\\/[^\\n]*), TokenType::COMMENT));5.3 现象Linux 下运行报qt.qpa.plugin: could not find the qt platform plugin xcb但libqxcb.so明明存在原因editBox.pro中LIBS -L$$PWD/../lib -lpublic这行第 38 行是作者遗留的错误引用——-lpublic对应一个不存在的库导致链接器污染了 RPATH使 Qt 插件路径查找失效。解决打开editBox.pro删除整行LIBS -L$$PWD/../lib -lpublic。Qt Widgets 应用默认依赖libQt5XcbQpa.so该库由 Qt 安装目录的plugins/platforms/提供无需额外链接。5.4 现象输入while (x 0) { x x - 1; }后AST 表中x x - 1的AssignExpr节点children只有一个子节点应为两个左x右SubExpr原因Parser::parseExpression()中parseTerm()返回的ParseNode是栈上对象node.children.append(left)存的是临时对象地址left作用域结束后地址悬空。解决ParseNode必须用堆分配。修改parseExpression()中相关代码// 替换原 left/right 声明 ParseNode* left new ParseNode(parseTerm()); while (...) { ParseNode* right new ParseNode(parseTerm()); ParseNode* node new ParseNode(); node-nodeType ...; node-children.append(left); node-children.append(right); left node; // now left points to heap } return *left; // 最后解引用返回值并在ParseNode析构函数中递归deletechildren防止内存泄漏。5.5 现象sqlite3api.cpp编译报错undefined reference to sqlite3_open原因editBox.pro中LIBS -lsqlite3缺失或系统 SQLite 库路径未加入LIBS。解决在editBox.pro末尾添加# Linux unix:!macx { LIBS -lsqlite3 } # Windows (MinGW) win32 { LIBS -lsqlite3 # 若 MinGW 未自带需指定路径LIBS -LC:/path/to/sqlite3/lib -lsqlite3 }然后右键项目 →Run qmake强制重载。6. 进阶技巧三步改造让你的分析器支持自定义语言语法与实时 AST 可视化6.1 步骤一替换文法——从 C-Lite 到你的 DSL以 JSON 解析器为例Parser类的硬编码文法是瓶颈。要支持 JSON只需重写parseValue()和parseObject()无需动框架。在mainwindow.cpp中新增// 新增方法parseJSON() ParseNode Parser::parseJSON() { if (currentToken.type TokenType::LBRACE) { return parseObject(); } else if (currentToken.type TokenType::LBRACKET) { return parseArray(); } else { return parsePrimitive(); } } ParseNode Parser::parseObject() { ParseNode node; node.nodeType NodeType::OBJECT; consume(TokenType::LBRACE); // expect { if (currentToken.type ! TokenType::RBRACE) { do { // key: string, value: any if (currentToken.type TokenType::STRING_LITERAL) { ParseNode key; key.nodeType NodeType::STRING; key.text currentToken.value.toStdString(); consume(); consume(TokenType::COLON); // : ParseNode value parseJSON(); node.children.append(new ParseNode(key)); node.children.append(new ParseNode(value)); } } while (currentToken.type TokenType::COMMA consume()); } consume(TokenType::RBRACE); return node; }然后在parseProgram()中调用return parseJSON();替代原有逻辑。这样输入{name: Alice, age: 30}就能生成标准 JSON AST。6.2 步骤二AST 可视化——用 QTreeWidget 替代表格实现折叠展开table.cpp的QTableView只能平铺显示无法体现树形结构。新建astview.cpp#include QTreeWidget #include QTreeWidgetItem class ASTView : public QTreeWidget { public: void setAST(const ParseNode root) { clear(); QTreeWidgetItem* top new QTreeWidgetItem(this); top-setText(0, nodeTypeToString(root.nodeType).c_str()); buildTree(root, top); expandAll(); } private: void buildTree(const ParseNode node, QTreeWidgetItem* item) { for (const auto child : node.children) { QTreeWidgetItem* childItem new QTreeWidgetItem(item); childItem-setText(0, nodeTypeToString(child-nodeType).c_str()); if (!child-text.empty()) { childItem-setText(1, QString::fromStdString(child-text)); } buildTree(*child, childItem); } } };在mainwindow.ui中拖入QTreeWidget提升为ASTView并在mainwindow.cpp的on_parseButton_clicked()中调用astView-setAST(astRoot)。从此双击节点即可展开子树AST 结构一目了然。6.3 步骤三错误定位高亮——在 QTextEdit 中标记错误行与列QTextEdit原生不支持语法高亮但我们可以用QTextCursor和QTextCharFormat实现错误定位。在mainwindow.cpp中添加void MainWindow::highlightError(int line, int column) { QTextCursor cursor(ui-textEdit-document()); cursor.movePosition(QTextCursor::Start); // 移动到指定行 for (int i 0; i line - 1 !cursor.atEnd(); i) { cursor.movePosition(QTextCursor::Down); } // 移动到指定列粗略因字体等宽 cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column); cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 10); // 高亮10字符 QTextCharFormat format; format.setBackground(Qt::red); format.setForeground(Qt::white); cursor.setCharFormat(format); }在Parser::addError()被调用时传入line和column触发此方法。用户一眼就能看到while (x 0) { x x - 1; }中哪一字符触发了错误。从那以后我每次教学生做编译原理课设都会让他们先在这个 Qt 分析器上跑通while循环再逐步加if-else、加function最后加自己的 DSL。因为只有当 AST 真正在屏幕上展开、错误红框真的跳出来他们才相信——编译器不是黑匣子是可触摸、可调试、可修改的代码。希望帮到你。本文还有配套的精品资源点击获取