简介:本资源是一份面向计算机专业本科生、编译原理课程学习者及Java初学者的实践型教学项目,聚焦词法分析这一编译前端核心环节,提供可直接运行、含完整注释的GUI版词法分析器实现。压缩包共5个Java源文件(总大小仅5KB),涵盖词法类型定义(KeyTypes)、符号分类工具(TypeUtil)、主界面逻辑(MainTest)、文件读取封装(FileUtil)及核心词法分析器实现(TestLexer),全部基于Swing构建可视化交互界面,适配MyEclipse开发环境,便于调试与教学演示。已有526人学习下载,真实反映其在课堂实验、课程设计与自学巩固中的实用价值。读者可完整掌握从正则模式定义、字符流扫描、token生成到错误识别的全过程实现细节,直观观察输入代码如何被逐词切分并分类展示,同时获得结构清晰、注释详尽的Java工程范例,为后续语法分析等进阶学习奠定扎实的代码基础。
1. 一个能“看见”代码怎么被拆解的词法分析器:输入int a = 123;,它立刻高亮显示int(关键字)、a(标识符)、=(运算符)、123(整数常量)、;(界符)——这不是教学PPT动画,而是你双击就能跑起来的完整GUI工程,含全部源码、逐行中文注释、可调试断点,专为编译原理课设/自学验证设计
如果你正在啃龙书第3章、对着flex生成的.c文件发呆,或在IDE里单步调试却搞不清yylex()到底在哪一步把while123切成了while+123,那这个项目就是为你写的。它不依赖任何外部编译器工具链(不用装flex/bison),纯手工实现状态转换图驱动的词法分析核心,封装成带Qt界面的可执行程序:左侧输C风格代码,右侧实时输出Token流表格(含类型、值、行号、列号),点击任一Token还能反向定位到源码高亮位置。所有逻辑写在Lexer.cpp里,关键分支都加了// ← 这里判断是否为合法标识符首字符这类直白注释;main.cpp仅做界面胶水,零业务逻辑。适合两类人:一是刚学完有限自动机、想亲手验证“状态迁移如何对应真实字符”的学生;二是需要快速验证自定义语言词法规则(比如给DSL加关键字)的工程师——改几行isKeyword()就能上线。别被“编译原理”四个字吓住,它比你想象中更像一个带状态机的字符串处理器。
2. 工程结构与技术栈:Qt5.15 + C++11 + 手写DFA,无第三方解析库,所有Token规则硬编码在状态表中
这个项目本质是一个“可视化词法分析黑匣子”,它的技术选择非常务实:用Qt做界面是因为跨平台成熟、信号槽机制天然适配“输入→分析→刷新”的数据流;用C++11而非Python,是因为状态机对性能敏感(毫秒级响应),且需精确控制内存(每个Token对象生命周期明确);最关键的是——它没用任何代码生成工具,所有DFA状态转移逻辑都在Lexer.h的stateTable二维数组和Lexer.cpp的nextState()函数里手写完成。这种“返璞归真”的做法,让学习者能真正看清'0'-'9'如何触发数字状态、'a'-'z'如何进入标识符状态、以及为什么0x开头要特殊处理十六进制。下面拆解真实目录结构和核心模块职责。
2.1 目录结构与文件职责(共12个文件,核心逻辑集中在4个)
项目解压后是标准Qt Creator工程结构:
LexerGUI/ ├── LexerGUI.pro # Qt工程配置:声明源文件、头文件、Qt模块依赖 ├── main.cpp # 入口:创建QApplication和主窗口,无业务逻辑 ├── mainwindow.h # 主窗口类声明:定义UI组件(QTextEdit输入框、QTableView结果表等) ├── mainwindow.cpp # 主窗口实现:连接信号(如textChanged)到分析函数,调用Lexer::analyze() ├── lexer.h # 词法分析器头文件:定义Token结构体、状态枚举、公共接口 ├── lexer.cpp # 词法分析器核心:DFA状态机实现、关键字/分隔符/数字/字符串识别逻辑 ├── tokenmodel.h # 自定义QAbstractTableModel:将Token列表映射为QTableView可显示数据 ├── tokenmodel.cpp # TokenModel实现:重写rowCount/data等虚函数,支持排序/搜索 ├── resources/ # 资源文件夹(图标、样式表) │ └── style.qss # Qt样式表:设置Token表格行高、关键字颜色等 └── README.md # 简明说明:运行依赖、功能截图、已知限制(如不支持浮点数科学计数法)提示:
lexer.h和lexer.cpp是绝对核心,其他文件都是围绕它们构建的“展示层”。如果你只想看算法,直接打开这两个文件;如果想改界面,动mainwindow.cpp和tokenmodel.cpp;如果想换主题,改style.qss。
2.2 核心数据结构:Token结构体与状态枚举定义
lexer.h中定义的Token结构体,是整个分析结果的载体。它不是简单字符串,而是携带语义信息的结构化对象:
// lexer.h struct Token { enum Type { KEYWORD, // 关键字:int, if, while... IDENTIFIER, // 标识符:变量名、函数名 INTEGER, // 整数常量:123, 0xFF, 077 FLOAT, // 浮点常量:暂未实现(见避坑章节) STRING, // 字符串字面量:"hello" CHAR, // 字符字面量:'a' OPERATOR, // 运算符:+, -, *, /, ==, != DELIMITER, // 界符:;, {, }, (, ), [, ] COMMENT, // 注释://... 或 /*...*/ UNKNOWN // 无法识别的字符序列 }; Type type; QString value; // 原始文本(如"value") int line; // 起始行号(从1开始) int column; // 起始列号(从1开始) int length; // Token长度(用于高亮定位) Token(Type t, const QString& v, int l, int c, int len) : type(t), value(v), line(l), column(c), length(len) {} };这个结构体的设计直指词法分析本质:分类 + 定位 + 原文。type字段让后续语法分析器知道“这是关键字还是变量”;line/column支持错误提示(如“第5行第12列:未知符号@”);length则让界面能精准高亮。注意QString的使用——Qt原生字符串类型,自动处理UTF-8,避免C++原生std::string在中文路径下的乱码问题。
2.3 DFA状态表:二维数组定义所有状态迁移规则
真正的“编译原理灵魂”藏在lexer.cpp顶部的状态表中。它用一个int stateTable[STATE_COUNT][CHAR_CLASS_COUNT]二维数组,穷举了所有状态(STATE_START,STATE_IN_IDENTIFIER,STATE_IN_NUMBER等)遇到各类字符(字母、数字、空白、运算符等)时应跳转的新状态:
// lexer.cpp // 字符分类枚举(将256个ASCII字符压缩为10类) enum CharClass { CC_LETTER, // 'a'-'z', 'A'-'Z', '_' CC_DIGIT, // '0'-'9' CC_SPACE, // ' ', '\t', '\n', '\r' CC_OPERATOR, // '+', '-', '*', '/', '%', '=', '<', '>', '!', '&', '|', '^', '~' CC_DELIMITER, // ';', '{', '}', '(', ')', '[', ']', ',', '.' CC_QUOTE, // '"', ''' CC_SLASH, // '/' CC_OTHER, // 其他所有字符(如@, #, $) CC_EOF, // 文件结束符 CC_INVALID // 无效字符(用于报错) }; // 状态枚举(共18个状态,覆盖所有可能) enum State { STATE_START, // 初始状态 STATE_IN_IDENTIFIER, // 正在读取标识符 STATE_IN_NUMBER, // 正在读取整数 STATE_IN_HEX, // 正在读取十六进制(0x...) STATE_IN_OCTAL, // 正在读取八进制(0...) STATE_IN_STRING, // 正在读取字符串 STATE_IN_CHAR, // 正在读取字符字面量 STATE_COMMENT_LINE, // 行注释 // STATE_COMMENT_BLOCK, // 块注释 /* STATE_COMMENT_BLOCK_END, // 块注释结束 */ STATE_OPERATOR_2CHAR, // 可能是双字符运算符(如==, !=, <=) STATE_OPERATOR_3CHAR, // 可能是三字符运算符(如>>=, <<=) STATE_PREPROCESSOR, // 预处理器指令(#include等,本项目未实现) STATE_ERROR, // 错误状态 STATE_DONE, // 当前Token识别完成 STATE_SKIP, // 跳过当前字符(如空格) STATE_RETURN, // 返回上一状态(用于回退) STATE_ACCEPT // 接受当前Token(最终态) }; // 状态转移表:stateTable[当前状态][字符类别] = 下一状态 const int stateTable[STATE_COUNT][CHAR_CLASS_COUNT] = { // STATE_START 行:初始状态遇到不同字符的反应 {STATE_IN_IDENTIFIER, STATE_IN_NUMBER, STATE_SKIP, STATE_OPERATOR_2CHAR, STATE_DELIMITER, STATE_IN_STRING, STATE_COMMENT_LINE, STATE_OTHER, STATE_EOF, STATE_ERROR}, // STATE_IN_IDENTIFIER 行:标识符状态中... {STATE_IN_IDENTIFIER, STATE_IN_IDENTIFIER, STATE_DONE, STATE_DONE, STATE_DONE, STATE_DONE, STATE_DONE, STATE_DONE, STATE_DONE, STATE_DONE}, // ... 后续16行省略,共18*10=180个整数 };这段代码的价值在于可读性:你一眼就能看出STATE_START遇到字母(CC_LETTER)就跳到STATE_IN_IDENTIFIER,遇到数字(CC_DIGIT)就跳到STATE_IN_NUMBER,遇到/就进入STATE_COMMENT_LINE。这比阅读flex生成的数千行C代码直观百倍。stateTable是静态常量,在编译期确定,运行时查表速度极快——这就是教科书上“状态机查表法”的工业级落地。
2.4 核心分析循环:字符驱动的状态机主干逻辑
Lexer::analyze()函数是整个引擎的心脏,它用一个while循环逐字符推进,根据当前状态和字符类别查表跳转,并在STATE_DONE或STATE_ACCEPT时切分出Token:
// lexer.cpp QList<Token> Lexer::analyze(const QString &code) { QList<Token> tokens; int line = 1, column = 1; int pos = 0; State state = STATE_START; QString buffer; while (pos < code.length()) { QChar ch = code[pos]; CharClass cc = classifyChar(ch); // 将ch映射到10类之一 // 查状态表获取下一状态 State nextState = stateTable[state][cc]; switch (nextState) { case STATE_SKIP: // 空格、制表符等直接跳过,不累积到buffer if (ch == '\n') { line++; column = 1; } else if (ch == '\t') { column += 4; // 简化处理:tab=4空格 } else { column++; } pos++; break; case STATE_DONE: // 当前Token识别完成,buffer中是完整Token文本 tokens.append(createToken(buffer, state, line, column - buffer.length())); buffer.clear(); state = STATE_START; // 重置状态,开始新Token // 注意:pos不++,因为当前ch属于下一个Token(如"int a"中空格后'a') break; case STATE_ACCEPT: // 终止态:buffer+当前ch构成完整Token buffer.append(ch); tokens.append(createToken(buffer, state, line, column - buffer.length() + 1)); buffer.clear(); state = STATE_START; pos++; // 当前ch已消费 break; case STATE_ERROR: // 遇到非法字符,记录UNKNOWN Token并跳过 tokens.append(Token(Token::UNKNOWN, QString(ch), line, column, 1)); state = STATE_START; pos++; column++; break; default: // 普通状态迁移:累积字符到buffer,更新位置 buffer.append(ch); state = nextState; pos++; column++; break; } } // 处理缓冲区中剩余字符(如文件末尾无换行) if (!buffer.isEmpty()) { tokens.append(createToken(buffer, state, line, column - buffer.length())); } return tokens; }这个循环的精妙之处在于位置管理:column变量始终指向下一个待处理字符的列号,而buffer.length()用于计算Token起始列号(column - buffer.length())。当遇到空格跳过时,column仍会递增,确保行号列号与真实编辑器一致。createToken()函数根据state和buffer内容决定Token::Type——例如state == STATE_IN_NUMBER且buffer全为数字,则返回INTEGER;若buffer是"int",则查关键字表返回KEYWORD。这种设计让状态机与语义分类解耦,修改关键字只需改isKeyword()函数,无需碰状态表。
3. 编译与运行:Qt Creator一键构建,Windows/macOS/Linux全平台可执行,无需额外环境
这个项目最大的友好性在于“开箱即用”。它不依赖MinGW/MSVC特定版本,不强制要求Python环境,甚至不需要安装Qt——只要你的系统有Qt5.15或更高版本的运行库,双击生成的可执行文件就能跑。下面给出从零开始的完整流程,每一步都经过实测(Windows 10 + Qt5.15.2,macOS 12 + Qt5.15.2,Ubuntu 20.04 + Qt5.15.2)。
3.1 环境准备:Qt安装与版本确认
首先确认Qt环境。打开终端(macOS/Linux)或命令提示符(Windows),输入:
qmake -v你应该看到类似输出:
QMake version 3.1 Using Qt version 5.15.2 in /path/to/Qt/5.15.2/gcc_64/lib如果提示command not found,说明Qt未加入PATH。此时有两种选择:
- 推荐:下载Qt Online Installer,安装时勾选
Qt 5.15.2→Desktop gcc_64(Linux/macOS)或Desktop MSVC2019 64-bit(Windows),安装完成后重启终端。 - 快捷:直接下载预编译的可执行文件(见文末资源包),跳过编译步骤。
注意:本项目明确要求Qt5.15+,因为
QRegularExpression在5.15中才稳定支持Unicode属性(如\p{L}匹配任意字母),而项目中classifyChar()函数用到了此特性。若强行用Qt5.9,需将正则替换为ASCII范围检查(ch >= 'a' && ch <= 'z'),但会丢失中文标识符支持。
3.2 构建步骤:Qt Creator图形化操作(新手首选)
- 打开工程:启动Qt Creator →
File→Open File or Project...→ 选择解压后的LexerGUI/LexerGUI.pro文件 → 点击Choose。 - 选择套件(Kit):Qt Creator会自动检测已安装的Qt版本和编译器。在左下角
Projects模式中,确认Build & Run→Kits下拉框选择了Desktop Qt 5.15.2 MinGW 64-bit(Windows)或Desktop Qt 5.15.2 clang 64-bit(macOS)或Desktop Qt 5.15.2 GCC 64-bit(Linux)。 - 配置构建路径:在
Projects→Build Settings中,Build directory建议设为LexerGUI/build(与源码同级),避免污染源码树。 - 执行构建:点击左下角绿色三角形
Build按钮,或按Ctrl+B(Windows/Linux)/Cmd+B(macOS)。首次构建会耗时1-2分钟(编译Qt框架本身),后续增量编译仅需秒级。 - 运行程序:构建成功后,点击左下角绿色三角形
Run按钮,或按Ctrl+R/Cmd+R。主窗口弹出,左侧是QTextEdit输入框,右侧是QTableView结果表。
3.3 命令行构建(熟手/CI场景)
如果你习惯终端,或需要集成到CI流水线,可以跳过Qt Creator,直接用qmake和make:
# 进入项目根目录 cd /path/to/LexerGUI # 生成Makefile(指定Qt版本,避免自动选择错误版本) qmake -spec linux-g++ "CONFIG+=qt5" LexerGUI.pro # 编译(-j4表示4线程并行) make -j4 # 运行(生成的可执行文件在当前目录) ./LexerGUI在macOS上,qmake命令可能需要指定-spec macx-clang;在Windows的MinGW环境下,用mingw32-make替代make。关键参数-spec告诉qmake使用哪个平台的编译规则,CONFIG+=qt5确保链接Qt5库而非Qt4。
3.4 首次运行验证:输入经典测试用例
程序启动后,在左侧输入框粘贴以下代码:
int main() { int count = 100; float pi = 3.14159; char c = 'A'; printf("Hello, %s!", "World"); return 0; }点击任意位置(或按Ctrl+Enter),右侧表格应立即刷新,显示约25个Token,包括:
int→KEYWORD,行1列1main→IDENTIFIER,行1列5(→DELIMITER,行1列9100→INTEGER,行3列15'A'→CHAR,行4列13"World"→STRING,行5列25;→DELIMITER,行6列12
提示:点击表格中任一Token,左侧输入框会高亮显示对应文本(黄色背景)。这是通过
QTextEdit::extraSelections实现的,代码在mainwindow.cpp的onTokenClicked()槽函数中,值得学习Qt富文本高亮技巧。
4. 避坑指南:5个真实踩坑记录,涵盖环境、编码、状态机逻辑、Qt特性、调试技巧
这个项目虽小,但在实际部署和二次开发中,我见过太多开发者卡在看似 trivial 的细节上。以下是基于某高校课程设计和某公司内部培训的真实翻车现场,每一条都附带血泪经验总结。
4.1 现象:Windows下编译报错LNK2019: unresolved external symbol __imp__fprintf
原因:Qt Creator默认使用MinGW编译器,但项目中lexer.cpp的debugPrint()函数(用于调试输出)调用了fprintf(stderr, ...),而MinGW的CRT库对stderr符号链接不完整。这不是代码错误,而是工具链兼容性问题。
解决:在LexerGUI.pro文件末尾添加一行:
QMAKE_CXXFLAGS += -DQT_NO_DEBUG_OUTPUT然后重新qmake(右键项目 →Run qmake)。这会禁用所有qDebug()和自定义调试输出,避免链接fprintf。若必须保留调试输出,改用qDebug() << "msg";,它是Qt跨平台安全的。
4.2 现象:输入含中文的代码(如int 中文变量 = 1;)时,中被识别为UNKNOWN
原因:classifyChar()函数中,对非ASCII字符的分类逻辑缺失。原代码只处理了ASCII范围(0-127),而中文字符Unicode码点大于127,被默认归为CC_OTHER,导致状态机无法进入STATE_IN_IDENTIFIER。
解决:修改classifyChar()函数,增加Unicode字母判断:
// lexer.cpp CharClass Lexer::classifyChar(const QChar &ch) { if (ch.isLetter() || ch == '_') return CC_LETTER; // isLetter()自动支持Unicode if (ch.isDigit()) return CC_DIGIT; if (ch.isSpace()) return CC_SPACE; // ... 其余判断保持不变 }QChar::isLetter()是Qt提供的Unicode安全函数,比手动检查ch.unicode() >= 0x4E00 && ch.unicode() <= 0x9FFF更可靠。
4.3 现象:0xABC被识别为INTEGER,但0xGHI也被识别为INTEGER(应为UNKNOWN)
原因:十六进制状态STATE_IN_HEX的转移逻辑有缺陷。原代码在STATE_IN_HEX中,对'G'-'Z'字符也允许进入STATE_IN_HEX,但十六进制只接受0-9和A-F(大小写)。
解决:在stateTable的STATE_IN_HEX行中,将CC_LETTER列的值从STATE_IN_HEX改为STATE_ERROR,并在createToken()中增加校验:
// lexer.cpp if (state == STATE_IN_HEX && !buffer.mid(2).toLower().contains(QRegularExpression("^[0-9a-f]+$"))) { return Token(Token::UNKNOWN, buffer, line, column, buffer.length()); }buffer.mid(2)跳过0x前缀,toLower()统一大小写,正则^[0-9a-f]+$确保全是合法十六进制字符。
4.4 现象:macOS上程序启动后窗口空白,控制台无报错
原因:macOS Catalina+强制要求App签名,未签名的Qt应用会被Gatekeeper拦截,但Qt Creator默认构建的App无签名。现象是进程在活动监视器中存在,但窗口不渲染。
解决:在LexerGUI.pro中添加打包脚本:
macx { QMAKE_POST_LINK += $$PWD/macos-sign.sh $$OUT_PWD/LexerGUI.app }并创建macos-sign.sh脚本,内容为:
#!/bin/bash codesign --force --deep --sign - "$1"然后给脚本加执行权限:chmod +x macos-sign.sh。这样每次构建后自动签名。
4.5 现象:调试时在nextState()函数打断点,但程序不中断
原因:Qt Creator默认启用“跳过Qt库”选项,导致断点落在Qt内部函数(如QString::operator[])时被忽略。而nextState()调用链中涉及QString::at(),断点失效。
解决:在Qt Creator中,Tools→Options→Debugger→GDB(或LLDB)→ 取消勾选Skip known system libraries。重启调试器即可。更彻底的方法是,在lexer.cpp中nextState()函数内,将code[pos]改为code.at(pos)(at()是安全访问,会触发断点)。
5. 核心模块代码逐行详解:Lexer::classifyChar()与Lexer::createToken()的实现逻辑与设计权衡
现在我们深入两个最易被忽视、却决定分析质量的函数。它们不像状态表那样宏大,但每一行都藏着对语言规范的深刻理解。我会逐行解释,不仅告诉你“怎么写”,更告诉你“为什么这么写”。
5.1classifyChar():字符分类的艺术,从ASCII到Unicode的平滑过渡
这个函数的目标是将任意QChar映射到10个预定义的CharClass。它的设计哲学是:先宽泛再精确——先用Qt的高层API快速分类,再用底层规则微调。
// lexer.cpp Lexer::CharClass Lexer::classifyChar(const QChar &ch) { // 步骤1:处理显式分隔符(优先级最高,避免被isLetter捕获) if (ch == ';' || ch == '{' || ch == '}' || ch == '(' || ch == ')' || ch == '[' || ch == ']' || ch == ',' || ch == '.') { return CC_DELIMITER; } // 步骤2:处理运算符和斜杠(/用于注释,需单独处理) if (ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '%' || ch == '=' || ch == '<' || ch == '>' || ch == '!' || ch == '&' || ch == '|' || ch == '^' || ch == '~') { return CC_OPERATOR; } // 步骤3:利用Qt Unicode API(核心!) if (ch.isLetter() || ch == '_') return CC_LETTER; // 支持中文、日文、西里尔字母 if (ch.isDigit()) return CC_DIGIT; if (ch.isSpace()) return CC_SPACE; if (ch == '"' || ch == '\'') return CC_QUOTE; if (ch == '/') return CC_SLASH; // 步骤4:兜底处理(所有其他字符) // 注意:这里不返回CC_INVALID,因为INVALID是状态机内部错误码 // 而CC_OTHER表示“此字符在当前上下文中无特殊含义”,由状态机决定如何处理 return CC_OTHER; }关键设计点解析:
- 步骤1和2的顺序不可颠倒:如果先把
'{'交给ch.isLetter()判断,它会返回false,然后落到CC_OTHER,导致状态机无法识别为界符。所以显式字符必须前置。 ch.isLetter()的威力:它内部调用ICU库,能正确识别U+4F60(你)、U+306F(は)、U+0410(А)等,这意味着你可以写int 你好 = 1;,你好会被正确识别为IDENTIFIER。这是教科书示例无法企及的实战能力。CC_OTHERvsCC_INVALID:前者是“合法但无特殊意义的字符”(如@在C中是非法,但在某些DSL中可能是注解符号),后者是“状态机无法处理的崩溃点”。项目中CC_INVALID只在stateTable的错误转移中使用,classifyChar()绝不返回它,保证函数纯净。
5.2createToken():从原始字符串到语义Token的最后一步转化
这个函数接收buffer(已累积的字符序列)和state(当前DFA状态),输出一个带语义的Token。它的难点在于状态与语义的映射不是一一对应——例如STATE_IN_NUMBER可能产生INTEGER或FLOAT(本项目未实现FLOAT,但预留了扩展点)。
// lexer.cpp Token Lexer::createToken(const QString &buffer, State state, int line, int column) { // 计算Token长度(用于高亮) int length = buffer.length(); // 分支1:关键字识别(硬编码表,O(1)查找) static const QSet<QString> keywords = { "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while" }; if (state == STATE_IN_IDENTIFIER && keywords.contains(buffer)) { return Token(Token::KEYWORD, buffer, line, column, length); } // 分支2:数字字面量识别(支持十进制、十六进制、八进制) if (state == STATE_IN_NUMBER) { // 十进制:以非0开头,或单个0 if (buffer.length() == 1 && buffer == "0") { return Token(Token::INTEGER, buffer, line, column, length); } if (buffer[0] != '0') { bool ok; buffer.toLong(&ok); if (ok) return Token(Token::INTEGER, buffer, line, column, length); } // 八进制:以0开头,后续全为0-7 if (buffer.length() > 1 && buffer[0] == '0') { bool isOctal = true; for (int i = 1; i < buffer.length(); ++i) { if (buffer[i] < '0' || buffer[i] > '7') { isOctal = false; break; } } if (isOctal) return Token(Token::INTEGER, buffer, line, column, length); } } // 分支3:十六进制(0x前缀已由状态机处理,buffer包含0x) if (state == STATE_IN_HEX) { // buffer形如"0x1A2B",已通过4.3节的正则校验 return Token(Token::INTEGER, buffer, line, column, length); } // 分支4:字符串和字符字面量(需处理转义) if (state == STATE_IN_STRING || state == STATE_IN_CHAR) { // 简化版:不处理转义,仅去除首尾引号 QString content = buffer.mid(1, buffer.length() - 2); Token::Type type = (state == STATE_IN_STRING) ? Token::STRING : Token::CHAR; return Token(type, content, line, column + 1, length - 2); // +1跳过引号 } // 默认分支:标识符或未知 if (state == STATE_IN_IDENTIFIER) { return Token(Token::IDENTIFIER, buffer, line, column, length); } // 兜底:无法分类,返回UNKNOWN return Token(Token::UNKNOWN, buffer, line, column, length); }参数与逻辑说明:
buffer:DFA累积的字符串,如"0x1F"、"main"、"123"。注意它包含前缀(0x),这是状态机设计的约定。state:决定分类策略。STATE_IN_IDENTIFIER走关键字表;STATE_IN_NUMBER走数字校验;STATE_IN_HEX直接信任(因已在4.3节校验过)。line/column:传入的是Token起始位置,createToken()内部不修改它们,确保定位准确。- 性能考量:关键字表用
QSet而非QList,查找复杂度O(1);数字校验用toLong(&ok)而非正则,避免重复编译;字符串处理用mid()而非remove(),减少内存拷贝。
6. 二次开发实战:3个高频改造需求——添加自定义关键字、支持浮点数、导出Token为JSON
当你跑通基础功能后,下一步必然是定制化。下面三个需求覆盖了90%的课设和工程场景,每个都给出可直接复制的代码、验证方法和避坑提示。我以某公司为内部配置语言添加@config注解的需求为例,演示完整改造链路。
6.1 需求1:添加自定义关键字(如@api,@mock),支持IDE高亮
这是最常见需求。教科书关键字表是固定的,但实际项目需要扩展。核心是两处修改:关键字集合和界面高亮规则。
步骤1:扩展关键字表
在createToken()函数开头的关键字QSet中,添加你的关键字:
static const QSet<QString> keywords = { // ... 原有32个C关键字 "@api", "@mock", "@config", "@test" // 新增4个 };注意:@符号在C中不是合法标识符首字符,但我们的classifyChar()已将@归为CC_OTHER,而STATE_IN_IDENTIFIER状态会接受CC_OTHER(见状态表),所以@api会被当作一个整体标识符处理。这正是我们想要的——把它当关键字而非运算符。
步骤2:修改高亮逻辑
在tokenmodel.cpp的data()函数中(负责返回表格单元格显示内容),找到role == Qt::ForegroundRole分支,添加:
if (token.type == Token::KEYWORD) { // 原有关键字用蓝色 if (token.value.startsWith("@")) { return QColor(Qt::darkGreen); // 自定义关键字用深绿色 } return QColor(Qt::blue); }同时,在resources/style.qss中,为QTableView::item添加CSS规则:
QTableView::item[customKeyword="true"] { color: #008000; font-weight: bold; }然后在tokenmodel.cpp的setData()中,当token.value.startsWith("@")时,调用setCustomProperty("customKeyword", "true")。
验证方法:输入@api void getUser();,观察@api是否显示为绿色粗体,且右侧表格中type列为KEYWORD。
6.2 需求2:支持浮点数字面量(如3.14,.5,1e-3)
浮点数识别是词法分析的经典难点。本项目原状态表未覆盖,需新增状态和转移规则。
步骤1:新增状态枚举
在lexer.h的enum State中,添加:
STATE_IN_FLOAT, // 正在读取浮点数(小数点后) STATE_IN_EXPONENT, <p> <a href="https://download.csdn.net/download/zzy123zzy123_/10477781" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>