不能直接用正则一次性匹配所有 token,因为 std::regex 难以控制偏移、不支持最长前缀匹配(如 == 优先于 =)、错误定位困难;手写状态机可精确控制字符流转、便于调试扩展,并避免重叠匹配、位置错乱、注释截断等问题。

为什么不能直接用正则一次性匹配所有 Token
因为 C++ 标准库的 std::regex 在多次匹配时难以精确控制偏移、无法原生支持「最长前缀匹配」(比如 == 必须优先于 =),且错误定位困难。手写状态机虽然代码量略增,但能严格控制每个字符的流转路径,便于调试和扩展。
常见错误现象:用多个 std::regex_search 轮询导致重叠匹配(如把 != 拆成 ! 和 =)、跳过空白后位置错乱、注释未被完整吞掉就提前结束。
- 状态机必须从当前索引出发,逐字符推进,不可回溯(除非显式保存位置)
- 每种状态(如
IN_NUMBER、IN_STRING)需独立处理转义、边界、非法字符 - 注释状态(
IN_LINE_COMMENT/IN_BLOCK_COMMENT)必须吞掉全部内容,直到换行或*/,否则后续 Token 位置全错
如何设计最小可行的状态转移表
不需要完整编译器级的 FSM,只需覆盖实际脚本语法子集:标识符、数字(整数/小数)、字符串字面量(双引号)、操作符(+ - == != &&)、括号、分号、注释。状态用枚举定义,转移逻辑收在单个 next_state() 函数里。
关键参数差异:字符输入是 char,但判断时要区分 ASCII 控制符(如 '\n')、空白(std::isspace)、数字(std::isdigit)、标识符首字符(isalpha 或 '_')——别直接用 std::isalnum 判标识符后续字符,它会把非 ASCII 字母也放行,破坏可移植性。
- 初始状态为
START,遇到空白/换行直接跳过;遇到/立即分支:下一个字符是/进IN_LINE_COMMENT,是*进IN_BLOCK_COMMENT,否则当除法操作符 -
IN_STRING中必须处理"hello \"world\"":遇\后读一个字符,不判结束,直接吞掉下个字符(哪怕它是") - 数字状态要区分
123、123.45、.45(有些脚本允许),但一旦出现e或E就进入指数部分,此时再跟字母就报错
Token 结构体怎么存原始文本才不踩内存坑
不能只存 std::string_view 指向源字符串的切片——如果用户传入的是临时 std::string(比如 lex("x=1;")),函数返回的 Token 里 std::string_view 会悬垂。也不能无脑拷贝成 std::string,性能敏感场景(如高频解析配置)开销明显。
折中方案:Token 定义里用 std::string_view + 可选的 owning buffer 标志位;实际构造时,若输入是持久生命周期的 const char* 或 std::string&,用 view;若是临时字符串,主动 std::string 拷贝。但更简单可靠的做法是:Lexer 类持有输入副本(std::string m_source),所有 Token 的 std::string_view 都指向它内部数据。
-
Token至少包含:type(枚举)、lexeme(std::string_view)、line、column - 不要在
Token里存语义值(如数字的double值)——那是后续语法分析阶段的事,词法层只负责“看见什么” - 字符串 Token 的
lexeme应包含两端引号(如"\"abc\""),方便调试;若需取内容,由上层调用unquote()处理
测试时最容易漏掉的边界情况
不是语法错误,而是状态机逻辑断点:比如空文件、连续空格、行首缩进含 tab、字符串末尾无引号、数字后紧跟字母(123abc 应拆成 INT(123) + ID("abc"),而非报错)、操作符连写(=== 应识别为 EQ + =,不是非法)。
性能影响常被忽略:每次循环都调用 std::isspace(c) 比查静态 bool 数组慢;对 ASCII 字符,用 (c >= '0' && c 替代 <code>std::isdigit 更快且无 locale 依赖。
- 必须测
"/* unclosed"—— 状态机应一直走到输入末尾,并报告「unclosed block comment」 - 必须测
"0x1F"—— 如果支持十六进制,0x后必须接 hex digit,0xG是非法 - 必须测换行符组合:
"\r\n"(Windows)、"\n"(Unix)、"\r"(旧 Mac)——列号重置逻辑要统一
真正麻烦的从来不是主干逻辑,而是这些散落在角落的字符组合。写完状态转移后,先拿 20 行边界 case 跑一遍,比后期 debug 十分钟卡在一个 '\r' 上强得多。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











