ARTICLE DETAIL

资讯详情

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

C++/Qt词法分析器实现指南:状态机与token化实战

C++/Qt词法分析器实现指南:状态机与token化实战 简介一份使用C和Qt实现的词法分析器课程设计资源面向编译原理学习者与需要完成期末课设的计算机专业学生解决从源码读取到关键字、标识符、数字、运算符等标记提取的完整流程问题。压缩包共30个文件约571KB以cpp/h源码、Qt界面文件ui/qrc、工程配置与文档为主其中6个cpp和4个h涵盖词法分析核心逻辑、主窗口与图形展示模块另含NFA/DFA状态图、图片素材及docx设计报告便于对照代码和文档理解实现细节。资源包含词法分析器核心实现、GUI运行程序与测试代码可视化展示标记生成过程并通过状态转换图辅助理解自动机原理项目采用模块化结构编译器前端初学者可借此掌握正则表达式匹配、状态转移表构造等知识点。已有188人学习浏览适合作为课程设计参考或编译原理实验入门资料。1. 这个 C/Qt 词法分析器 zip 包解决的是你写编译器前端时最枯燥的一段如果你下载过「使用c qt 写的词法分析器.zip」这种源码包多半是打算在编译原理课设或脚本语言前端里把最容易被低估的一步落地。词法分析器的作用一句话就能说清把源代码字符流啃成 token。关键字、标识符、数字、字符串、运算符每种都要找对边界。看起来只是 while 加 switch但真正跑起来你会发现注释、转义、浮点、行号每一个都能让程序翻车。这个 zip 的价值不是背理论而是给出一条用 C 做核心、Qt 做界面的分工路径。适合想写 IDE 高亮、小解释器或者想理解编译前端的人。下面按你实际动手的顺序把原理、代码、编译和坑一次讲完。2. 拆开 zip 之前先立框架词法分析器的核心结构与 C/Qt 分工拿到 zip我建议先别急着 F5 编译先看工程里哪些文件管扫描、哪些文件管界面。很多课程设计把词法逻辑和窗口控件搅在一个文件里跑能跑但你想加一个命令行版就等于重写。好的 C/Qt 词法分析器骨架一定有一条清晰的分工线词法核心用普通 C 类Qt 只负责输入输出和事件交互。2.1 词法分析器在编译前端的位置从字符流到 token 流编译器前端通常分两段词法分析和语法分析。词法分析器消费的是原始字符产出的是 token 流语法分析器只认 token 流不再关心空格、注释和换行。为什么一定要有这一步因为语法规则描述的是“语句由标识符、等号、表达式组成”而不是“字符从第 3 位到第 7 位不能是数字”。如果跳过词法层语法分析器就得自己处理“123abc 是不是合法数字”这类问题文法里全是字符级噪声调试时根本分不清是词法错误还是语法错误。一个 token 在工程上至少带三样信息类型、词素lexeme、位置。类型决定它能否出现在当前语法位置词素保留源代码里的原始文本供后续生成中间代码或报错时引用行号列号用于定位。很多初版代码只存类型和文本到后面想加“第几行出错”才发现没有位置信息这属于后悔药都买不到的设计。我一般会在第一版就把 line 和 column 放进 Token虽然前几百行代码会多几个变量但后面做报错、做断点、做高亮都会轻松很多。实现方式上手工状态机比 flex 这类生成器更适合 C/Qt 工程。原因很现实zip 的读者大概率只装了 Qt Creator不想再配一套 flex 工具链。手工扫描就是用一个 pos 指针在 QString 上滑动按当前字符的类别分派到不同的读取函数遇到字母下划线就读标识符遇到数字就读数值遇到引号就读字符串。另一个原因是优先级规则容易控制。以if为例正确的做法是先把连续的字母都收进来形成一个标识符“if”再查关键字表把它改成 Keyword 类型。如果你反过来先匹配关键字再用正则处理标识符长度优先级和边界字符会让你绕很多路。2.2 Qt 在这类项目里负责什么QString、信号槽与界面刷新很多刚接触 Qt 的人会误以为“用 C Qt 写词法分析器”意味着词法核心必须用 Qt 的类。其实 Qt 在这里的角色是外挂不是内核。词法扫描完全可以只用纯 C 做但 Qt 的三样东西让工程好写得多。第一样是 QString。界面上的 QPlainTextEdit 直接返回 QString如果词法核心用 std::string每次调用都要 toString 再 toStdString编码转换一旦碰上中文就出乱码。QString 内部是 UnicodeQChar 自带 isLetter、isDigit、isSpace 判断比 C 标准库的 ctype 在处理宽字符时更符合直觉。所以我的习惯是整个 Lexer 内部统一用 QString只在文件读写边界做一次 UTF-8 转换。第二样是信号槽。按钮点击、文件打开、文本变化这些事件在 Qt 里通过 connect 接到槽函数。典型的流程是点“分析”按钮槽函数拿到输入文本构造 Lexer调用 tokenize再把结果刷到 QTableWidget 里。这里的 Q_OBJECT 宏和 moc 会引入编译期陷阱后面章节专门说。第三样是界面刷新。词法分析如果只跑几千行放 GUI 线程没问题。但这类项目做完后你一定会拿去打开一个几 MB 的源码文件扫描循环一跑窗口白屏操作系统觉得程序未响应。工程上通常把 tokenize 放到 QtConcurrent::run 或 QThread 里结果通过信号传回主线程。这个改造不难前提是你别把 Lexer 写成 QWidget 的成员函数别在 Lexer 里直接改控件。只要保持 Lexer 是“输入 QString输出 QVector ”的纯计算类后台化就是一个线程壳的事。2.3 典型文件组织一个最小 C/Qt 词法分析器的模块边界我见过的大多数能长期改下去的 C/Qt 词法分析器文件分层都长得差不多。下面这个表可以作为你检查 zip 内部结构的对照文件职责token.hToken 结构体、TokenType 枚举、类型转字符串函数lexer.h / lexer.cpp扫描循环、状态推进、read 系列方法mainwindow.h / mainwindow.cppQt 窗口、按钮、表格展示main.cppQApplication 入口、窗口实例化CMakeLists.txt 或 .pro构建脚本、Qt 模块声明关键点是依赖方向。mainwindow 依赖 lexer 和 tokenlexer 依赖 token 和 QtCore 的 QString/QVector但不依赖 QtWidgets。如果 zip 里所有代码都堆在 mainwindow.cpp先别急着重写把 Lexer 抽出来这一步是最值得做的因为后续加命令行测试和单元测试都要靠它。再有一个选型问题Lexer 该不该继承 QObject我的回答是不要除非你要在 Lexer 里发信号。普通 C 类做纯计算不需要 moc不需要考虑“哪个线程创建它就必须在哪个线程使用”也不用被 Q_OBJECT 的编译步骤绊住。信号槽放在 MainWindow 或一个独立的 Worker 类里就够了。这个边界一旦画清楚AUTOMOC、多线程、编码转换这些 Qt 玄学基本不会找到你头上。3. 用最小 C/Qt 词法分析器跑通关键字、标识符、数字与字符串代码与逐段说明下面这套最小实现我通常用来当模板覆盖了关键字、标识符、整数、浮点、字符串、运算符和注释。你可以在 Qt Creator 里新建 Qt Widgets 项目把这些文件覆盖进去直接编译即可。文件不多但每段注释我都尽量写到“为什么”不是只写“是什么”。3.1 Token 与 TokenType 定义让分类先固定下来先写 token.h。这是整个分析器的“公共接口”语法分析器将来也要 include 它。// token.h #ifndef TOKEN_H #define TOKEN_H #include QString enum class TokenType { Identifier, Keyword, IntegerLiteral, FloatLiteral, StringLiteral, Operator, EndOfFile, Error // 词法错误例如未闭合的字符串 }; struct Token { TokenType type; QString lexeme; // 从源码里切下来的原始文本或字符串值 int line; int column; }; QString tokenTypeName(TokenType type); // 用于界面展示 #endif这里用 enum class 而不是普通 enum是为了避免 TokenType::Identifier 和全局的 Identifier 名字冲突C 项目大一点之后这一点特别重要。lexeme 用 QString 而不是 std::string理由前面说过QString 在整个 Qt 生态里不用转换显示到表格、写 QDebug 都方便。line 和 column 直接放在 Token 里面不要用两个平行的 QVector 去存否则排序和配对时你会先疯掉。3.2 词法分析核心扫描循环与状态判定词法核心的类声明如下// lexer.h #ifndef LEXER_H #define LEXER_H #include QString #include QVector #include token.h class Lexer { public: explicit Lexer(const QString sourceText); QVectorToken tokenize(); private: QString source; // 待分析的源码 int pos 0; // 当前扫描位置 int line 1; // 当前行号 int column 1; // 当前列号 void advance(); // 前进一个字符维护行列号 void skipSpacesAndComments(); // 跳过空白与注释 Token readIdentifierOrKeyword(); Token readNumber(); Token readString(); Token readOperator(); }; #endiftokenize 是整个循环的外层入口每次根据当前字符选择读法读到一个 Token 就追加到 QVector直到字符串末尾最后补一个 EndOfFile 标记。实现如下// lexer.cpp #include lexer.h #include QChar #include QSet Lexer::Lexer(const QString sourceText) : source(sourceText) { } void Lexer::advance() { if (pos source.size()) return; if (source.at(pos) QLatin1Char(\n)) { line; column 1; } else { column; } pos; } QVectorToken Lexer::tokenize() { QVectorToken tokens; static const QSetQString keywords { QStringLiteral(int), QStringLiteral(float), QStringLiteral(if), QStringLiteral(else), QStringLiteral(while), QStringLiteral(return) }; while (pos source.size()) { QChar ch source.at(pos); if (ch.isSpace() || ch QLatin1Char(#)) { skipSpacesAndComments(); continue; } if (ch.isLetter() || ch QLatin1Char(_)) { tokens.append(readIdentifierOrKeyword(keywords)); } else if (ch.isDigit()) { tokens.append(readNumber()); } else if (ch QLatin1Char()) { tokens.append(readString()); } else { tokens.append(readOperator()); } } tokens.append(Token{TokenType::EndOfFile, QStringLiteral(EOF), line, column}); return tokens; }这段的写法是典型的手工状态机骨架。static 的 keywords 集合定义在 tokenize 内部不会每次调用重新构造readIdentifierOrKeyword 读完后查这个集合命中就是 Keyword否则是 Identifier。这里把#也交给 skipSpacesAndComments 处理是为了方便跳过行注释如果你不想支持#把判断条件去掉即可。接下来是几个读取函数。注意 readNumber 里对小数点的处理我特意限制“小数点后面必须是数字”这样1.不会当成浮点数1.而是先读出整数1再把点放回主循环当运算符。这个边界行为是很多实现翻车的地方提前说清楚。Token Lexer::readIdentifierOrKeyword(const QSetQString keywords) { int startLine line; int startColumn column; QString text; while (pos source.size()) { QChar ch source.at(pos); if (ch.isLetterOrNumber() || ch QLatin1Char(_)) { text.append(ch); advance(); } else { break; } } TokenType type keywords.contains(text) ? TokenType::Keyword : TokenType::Identifier; return Token{type, text, startLine, startColumn}; }允许数字出现在标识符的后续字符里但不允许数字开头这是 C 系语言的标准规则。如果你希望标识符带$加一个ch QLatin1Char($)的判断即可。Token Lexer::readNumber() { int startLine line; int startColumn column; QString text; bool isFloat false; while (pos source.size()) { QChar ch source.at(pos); if (ch.isDigit()) { text.append(ch); advance(); } else if (ch QLatin1Char(.) !isFloat pos 1 source.size() source.at(pos 1).isDigit()) { isFloat true; text.append(ch); advance(); } else { break; } } TokenType type isFloat ? TokenType::FloatLiteral : TokenType::IntegerLiteral; return Token{type, text, startLine, startColumn}; }这里还没有处理指数形式1e10。做课程设计通常够用但如果你要兼容 C 语言浮点数建议在读到e或E后继续吞数字同时在1e时回退。这个回退逻辑容易写崩等你说要支持它时我们再展开。Token Lexer::readString() { int startLine line; int startColumn column; QString text; advance(); // 跳过开头的双引号 bool escaped false; while (pos source.size()) { QChar ch source.at(pos); if (escaped) { if (ch QLatin1Char(n)) text.append(QLatin1Char(\n)); else if (ch QLatin1Char(t)) text.append(QLatin1Char(\t)); else if (ch QLatin1Char(\\)) text.append(QLatin1Char(\\)); else if (ch QLatin1Char()) text.append(QLatin1Char()); else text.append(ch); // 未知转义原样保留 escaped false; advance(); continue; } if (ch QLatin1Char(\\)) { escaped true; advance(); } else if (ch QLatin1Char()) { advance(); return Token{TokenType::StringLiteral, text, startLine, startColumn}; } else { text.append(ch); advance(); } } return Token{TokenType::Error, text, startLine, startColumn}; // 没遇到右引号 }这里的核心是escaped标志。遇到反斜杠时先不决定下一个字符是什么而是把状态置为“转义中”下一次循环再处理n、t、\\、\。不这样做的话字符串里出现\时函数会在右引号之前提前结束后续所有字符全部被误判成运算符或标识符。这是词法分析器最经典的翻车现场之一。3.3 Qt 界面接入分析按钮、表格展示与信号槽核心类写完界面就薄了。界面布局用 Qt Designer 拖拽或代码手写都可以下面用代码方式便于 git 管理。一个带“分析”按钮和表格的窗口如下// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow class QPlainTextEdit; class QTableWidget; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void analyze(); private: QPlainTextEdit *sourceEdit; QTableWidget *tokenTable; }; #endif// mainwindow.cpp #include mainwindow.h #include QPlainTextEdit #include QTableWidget #include QPushButton #include QVBoxLayout #include lexer.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *central new QWidget(this); auto *layout new QVBoxLayout(central); sourceEdit new QPlainTextEdit(central); tokenTable new QTableWidget(central); tokenTable-setColumnCount(4); tokenTable-setHorizontalHeaderLabels( {QStringLiteral(类型), QStringLiteral(词素), QStringLiteral(行), QStringLiteral(列)}); auto *button new QPushButton(QStringLiteral(分析), central); connect(button, QPushButton::clicked, this, MainWindow::analyze); layout-addWidget(sourceEdit); layout-addWidget(button); layout-addWidget(tokenTable); setCentralWidget(central); resize(800, 600); } void MainWindow::analyze() { Lexer lexer(sourceEdit-toPlainText()); QVectorToken tokens lexer.tokenize(); tokenTable-setRowCount(tokens.size()); for (int i 0; i tokens.size(); i) { const Token tok tokens.at(i); tokenTable-setItem(i, 0, new QTableWidgetItem(tokenTypeName(tok.type))); tokenTable-setItem(i, 1, new QTableWidgetItem(tok.lexeme)); tokenTable-setItem(i, 2, new QTableWidgetItem(QString::number(tok.line))); tokenTable-setItem(i, 3, new QTableWidgetItem(QString::number(tok.column))); } }这段代码里connect(button, QPushButton::clicked, this, MainWindow::analyze);是 Qt 5 之后推荐的函数指针语法。如果你在老工程里看到SIGNAL(clicked())宏写法编译期不会检查函数签名是否匹配运行时一旦拼错就是一个静默失效。Q_OBJECT 宏在这里是必须的因为它声明了analyze是槽如果你把 MainWindow 的类声明写成没有 Q_OBJECTmoc 不会为它生成元对象代码connect 时虽然能过编译点按钮却没有任何反应。这里 tokenTypeName 需要你自己在 token.cpp 里写一个 switch把每个枚举值转成 QString。我习惯把它放在 token.cpp 而不是 lexer.cpp因为“类型怎么显示”是数据模型的一部分不该被词法扫描逻辑垄断。4. 编译这个 zip 工程Qt 版本、CMake/qmake 与三个必调参数代码写对了能不能跑起来又是另一件事。词法分析器这个 zip 如果用 Qt Creator 打开通常会带 .pro 或 CMakeLists.txt。我推荐用 CMake因为 Qt 6 时代它已经是官方主推而且跨平台不用改。下面把编译和发布这条路上的三个必调参数一次说清。4.1 先确认 Qt 与编译器版本避免 cannot mix incompatible Qt libraryQt Creator 里坑最大的不是语法错误而是环境串味。你机器上可能装过 Qt 5.6.1 和 Qt 5.15.2甚至还有 Qt 6系统 PATH 里一旦残留了老版本 Qt 的 binCMake 或 qmake 找到的头文件和运行时找到的 DLL 就不是同一套。典型的报错是fatal: cannot mix incompatible Qt library (version ex50601) with this librar前面是 Qt 5.6.1 的版本标记后面链接的库可能来自 5.15.2。遇到这个第一件事不是改代码而是确认当前构建到底用的是哪套 Qt。我一般会执行qmake --version cmake --version再在 Qt Creator 的“构建套件”里看 Qt 版本路径确保 qmake、编译器、构建目录三者一致。如果用的是 MSVC 套件机器上还要有对应版本的 Microsoft Visual C Redistributable不然运行时会报 0xc000007b 之类的错。如果还是混删掉 build 目录下的 CMakeCache.txt 和 Makefile重新配置一次。这个玄学问题九成是缓存导致的别问我为什么血泪经验。还有一个隐蔽来源环境变量 CMAKE_PREFIX_PATH。如果你在系统变量里写死了一个旧 Qt 路径CMake 的 find_package 会优先拿它。解决方法是把 CMAKE_PREFIX_PATH 改成当前要用的 Qt 安装路径例如 Qt 5.15.2 的 MSVC 2019 目录。这个参数是第一个必调参数。4.2 CMake 配置示例AUTOMOC、C17、find_package下面这份 CMakeLists.txt 兼容 Qt 5 和 Qt 6最小可跑cmake_minimum_required(VERSION 3.16) project(LexerDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) add_executable(LexerDemo main.cpp mainwindow.cpp mainwindow.h lexer.cpp lexer.h token.cpp token.h ) target_link_libraries(LexerDemo PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)第二个必调参数是CMAKE_AUTOMOC ON。mainwindow.h 里有 Q_OBJECTmoc 需要为它生成元对象代码。automoc 打开后CMake 会自动扫描头文件列表里的 Q_OBJECT不需要你再手动写 qt5_wrap_cpp。忘掉这个参数编译阶段不一定报错但链接时会找不到 vtable或者运行时点击按钮没有反应。第三个必调参数是CMAKE_CXX_STANDARD 17。词法分析器用到的 enum class、QStringLiteral、结构化绑定至少需要 C11设为 17 只是给将来留余地。不要把它设成 98Qt 6 本身都要求 C17。如果 zip 里带的是 .pro等价写法是QT widgets CONFIG c17 TARGET LexerDemo SOURCES main.cpp mainwindow.cpp lexer.cpp token.cpp HEADERS mainwindow.h lexer.h token.hqmake 会对头文件里的 Q_OBJECT 自动跑 moc但前提是这些头文件在 HEADERS 里列出来少列一个Qt Creator 经常不报编译错只在运行时出现奇怪崩溃。4.3 运行与发布windeployqt 处理 qt.qpa.plugin编译通过在 Qt Creator 里点运行没问题。但如果直接跑到构建目录下双击 exe很多新手会看到qt.qpa.plugin: could not find the Qt platform plugin windows in 这是因为 Qt 是插件化架构窗口平台底层由 platforms 目录里的 qwindows.dll 提供。Qt Creator 运行时会自动把 Qt 的 bin 目录加进 PATH所以你感觉不到脱离 Creator 后系统找不到插件就崩。解决办法是用 Qt 自带的命令行工具 windeployqtcd build/Release windeployqt LexerDemo.exe这条命令会从当前 PATH 里的 Qt 目录拷贝依赖的 DLL、插件和 qml 文件夹。如果你在配置环境时不小心把 PATH 指向了旧 Qtwindeployqt 也会复制一堆不兼容的文件所以跑之前先执行qmake --version确认版本。在嵌入式 Linux 上报错信息里的插件名会变成linuxfb缺的是libqlinuxfb.so解决思路一样检查QT_QPA_PLATFORM_PLUGIN_PATH环境变量是否指向插件目录或者干脆用export QT_QPA_PLATFORM_PLUGIN_PATH/path/to/plugins/platforms临时指定。这个环境变量是很多运行期怪问题的黑匣子遇到先查它。5. 词法分析器避坑清单5 个让程序闪退或误判的细节写词法分析器的过程里真正浪费时间的不是状态机设计而是那些编译期不报错、运行期才暴露的 Qt 细节。下面五条都是我在调这种 zip 工程时至少遇到过一次的按排查顺序列出来。5.1 现象启动时报 could not find the Qt platform plugin现象程序一启动就弹错说找不到 platform plugin windows或linuxfb。原因可执行文件旁边少了 platforms 目录或者 QT_QPA_PLATFORM_PLUGIN_PATH 指向错误。解决在构建目录的 exe 旁放好 Qt 提供的 platforms最简单是运行 windeployqt。手动复制时注意不是把 qwindows.dll 直接放在 exe 旁而是要放在 platforms/qwindows.dll 这个二级目录里这是最容易犯的路径错误。如果你配置过 QT_QPA_PLATFORM_PLUGIN_PATH 且值写错了程序会把旧路径当成唯一候选这时删掉该环境变量恢复默认搜索顺序。5.2 现象编译链接报 cannot mix incompatible Qt library现象CMake 或 make 阶段报cannot mix incompatible Qt library后面带一串版本号。原因头文件来自 Qt A链接库来自 Qt B版本不一致。常见于升级了 Qt 版本但没有清理构建缓存或 PATH 里残留旧 Qt 的 bin 目录。解决先确认 qmake --version 和 cmake 找到的 Qt 路径删除 build 目录下的 CMakeCache.txt在 Qt Creator 里重新配置项目如果还不行打开环境变量编辑器把 PATH 里所有不是当前版本的 Qt bin 删除重开终端。不要试图在代码里加宏去压制那个版本宏是保护机制的压掉只会让程序运行时崩得更难查。5.3 现象字符串里的 让 token 流错位现象输入a\b后词法分析器把 token 拆成字符串 a\ 和一个标识符 b之后整段代码分析全部错位。原因readString 里没有处理反斜杠转义遇到第一个就认为字符串结束。解决像第三章节里那样加escaped标志。这个 bug 在“能跑”阶段不会出现因为普通字符串都正常一旦出现转义所有后继代码的 token 类型全错而且错误位置离真正源头很远不好查。我习惯在写完 readString 后立刻用三个测试用例验证a\\、a\b、\\\。5.4 现象打开大文件后窗口卡死现象把一个 5MB 的源码文件粘贴进去点分析界面变白标题栏出现“未响应”。原因tokenize 是纯 CPU 密集型循环跑在 GUI 主线程里阻塞了消息循环。解决把 tokenize 调用移到后台线程。最简单方案是用 QtConcurrent::runQtConcurrent::run([this]() { Lexer lexer(sourceEdit-toPlainText()); auto tokens lexer.tokenize(); QMetaObject::invokeMethod(this, [tokens]() { // 回到主线程更新表格 }); });注意 lambda 捕获的 sourceEdit 要在主线程读取因为 mainwindow 的对象生命周期在主线程纯计算部分可以拿 QString 的值捕获避免跨线程访问控件。如果你连 QtConcurrent 都不想依赖用 QThread 子类把 Lexer 包起来也一样核心思想是 Lexer 本身不碰 QWidget。5.5 现象中文注释和字符串乱码现象源代码里有中文注释分析后显示在表格里变成乱码或者明明用 UTF-8 保存的文件读进来第一段就没对。原因用 std::ifstream 以本地编码读文件又转成 QString在 Windows 简体中文环境下默认是 GBK而 Qt 源码常按 UTF-8 处理。解决读文件时直接用 Qt 的 QFile 和 QString::fromUtf8不要中间过 std::stringQFile file(source.txt); if (file.open(QIODevice::ReadOnly)) { QString code QString::fromUtf8(file.readAll()); // 把 code 交给 Lexer }写出去做调试日志时也统一用 qDebug().noquote() token.lexeme别用 printf 配 std::string。Qt 的 QString 转 printf 是经典乱码来源能避免就避免。6. 把它扩展成你自己的语言前端加 token、加运算符、做回归验证这个 C/Qt 词法分析器骨架真正值钱的地方在于它能被一步步扩展成自己的小语言前端。下面说两个最常做的扩展以及我怎么验证它们没有把老功能改坏。6.1 向运算符表里加、-、这类多字符运算符在 readOperator 的 twoCharOps 集合里追加就行。比如static const QSetQString twoCharOps { QStringLiteral(), QStringLiteral(!), QStringLiteral(), QStringLiteral(), QStringLiteral(), QStringLiteral(||), QStringLiteral(), QStringLiteral(-), QStringLiteral(*), QStringLiteral(/) };注意--和要不要同时支持取决于你的语言设计。如果你支持自增应该在 readOperator 里被识别成运算符而不是两个。往集合里加字符串之前想清楚“最长匹配”规则从 pos 位置取出两个字符先查双字符表查不到才退化为单字符。上面的代码就是这么写的改表就能加运算符逻辑不用动。6.2 用一碗测试文本做回归验证把 token 流打印出来对答案我一般不用界面验证而是写一个纯函数在 main 里跑void runSelfTest() { Lexer lexer(QStringLiteral( int a 10;\n if (a 5) return 1;\n float f 3.14; // 测试注释\n string s \a\\\b\;\n )); QVectorToken tokens lexer.tokenize(); for (const Token tok : tokens) { qDebug().noquote() tokenTypeName(tok.type) tok.lexeme tok.line tok.column; } }然后把输出和自己手写的期望表对照。例如前两行应该分别出现 Keyword/Identifier/Operator/IntegerLiteral 这样的组合。词法分析器的小 bug 非常适合用最简单的方法暴露打印 token 流看看是否在期望的位置多了或少了一个 token。等这套自检稳定了再封装成 QTest 的单元测试每次改词法规则跑一遍能省掉大量手工点按钮的时间。我自己的习惯是改完词法规则先跑四个最翻车的输入空文件、只有一个左引号、1.、/* 未闭合。这四个用例过了再谈加新特性。把状态机写进文本框之前先在测试函数里把边界钉死这比任何调试器都好用。这个词法分析器虽然小但把字符流变 token 流这件事做踏实了后面语法分析就是顺着这条路继续走下去的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表