手写状态机比正则更可控,因std::regex易在嵌套注释、多行字符串、进制前缀等场景回溯爆炸或漏匹配;状态机显式控制每字符转移,可调试、加日志、中断,适合教学与嵌入式解析。

为什么手写状态机比正则更可控
因为 C++ 标准库不带内建的词法分析器,std::regex 在处理嵌套注释、多行字符串、前缀依赖(如 0x 和 0 开头数字)时容易漏匹配或回溯爆炸。状态机把每个字符的转移逻辑显式写出,每一步都可调试、可加日志、可中断——尤其适合教学、配置脚本或嵌入式轻量解析。
常见错误现象:tokenize("123abc") 返回 NUMBER 而不是 NUMBER + IDENTIFIER;"/* comment */ int x;" 中注释吞掉后续换行导致 int 错位;"012" 被误判为十六进制。
- 状态必须区分「已读数字但未确认进制」和「已确认是十进制/八进制/十六进制」
- 字符串状态需记录起始引号类型(
'还是"),并处理转义符\后是否换行 - 注释状态中,
//遇到换行即终止,/*必须严格匹配*/,不能用贪心匹配
核心状态枚举与转移表怎么组织才不易崩
别用 switch(state) { case S_START: ... } 堆满整个函数——分支一多就难维护。推荐用二维数组或 std::array<:array>, N_STATES></:array> 表达转移,索引是当前状态 + 当前字符的 ASCII 值。
实际使用中,State 枚举要预留 S_ERROR 和 S_DONE,且所有非法转移默认落到 S_ERROR,避免静默跳过坏字符。
立即学习“C++免费学习笔记(深入)”;
-
S_START遇空格/制表符/换行 → 仍为S_START(跳过空白) -
S_START遇/→ 进S_SLASH;再读一个/→ 切到S_LINE_COMMENT,读*→ 切到S_BLOCK_COMMENT -
S_NUMBER遇.且尚未有小数点 → 进S_FLOAT_DOT;若已有一个小数点,应直接报错或截断 - 所有字符串/注释状态中,遇到 EOF(
'\0')必须触发错误或强制结束,否则while循环卡死
Token 输出时机和缓冲区管理的关键细节
状态机本身不生成 Token,只决定「何时切分」和「当前 token 类型」。真正构造 Token 对象应在状态退出时,比如从 S_ID 回到 S_START 的瞬间,把缓存的字符序列拷贝成 std::string 并标注 TOKEN_IDENTIFIER。
容易踩的坑:std::string 缓冲区在每次新 token 开始前没清空;或者用指针记录起始位置但忘了保存长度,导致越界读;又或者在 S_NUMBER 中把前导零当无效而提前截断,破坏八进制语义。
- 用两个索引
start_pos和cur_pos记录当前 token 字符范围,避免频繁拷贝 - 数字 token 解析完成后,调用
std::stoll(buf, nullptr, base)前先检查base是否合法(2/8/10/16),否则抛异常 - 标识符不能以数字开头,但允许下划线和字母组合;关键字(如
if、while)需在产出后查哈希表映射为对应TOKEN_IF等常量 - 单字符运算符(
+、-、=)在S_START直接返回,不进缓冲区
如何让代码跑起来又不被编译器优化掉关键逻辑
最简可用版本只需一个 class Lexer,含 std::string_view input、size_t pos、std::vector<token> tokens</token>。主循环是 while (pos ,其中 <code>advance() 就是状态转移函数。
测试时最容易忽略的是边界:空输入、全空白、单字符、末尾无换行、UTF-8 多字节字符混入(此时需禁用或明确说明只支持 ASCII)。若未来要支持 Unicode,状态机里就不能用 char 做索引,得先做 UTF-8 解码。
- 写单元测试时,对
" \t\n// hello\n123"这类混合输入,断言tokens.size() == 2且第二个token.type == TOKEN_NUMBER - 用
g++ -O0 -g编译,方便 gdb 单步看state变量变化;开-Wall -Wextra捕获未处理的enum分支 - 不要在状态函数里做 I/O(如
std::cout),调试用fprintf(stderr, "...")或日志宏,否则干扰性能测试
状态机真正的复杂点不在转移逻辑,而在「什么时候该结束当前 token」和「下一个字符要不要还回去」——比如读到 == 时,第一个 = 进 S_EQ,第二个 = 触发完成,但若下一个是 a,就得把第二个 = 推回输入流。这个“回退”操作,是绝大多数手写 lexer 出 bug 的地方。


















