手写状态机比正则更可控,因std::regex在嵌套注释、多行字符串、换行敏感关键字等场景易漏匹配或回溯爆炸;状态机显式控制每字符转移,可调试、加日志、中断,且能精确处理上下文依赖(如/、"、的含义),适合教学与嵌入式解析。

为什么手写状态机比用正则更可控
因为 C++ 标准库不带内建词法分析器,而 std::regex 在处理嵌套注释、多行字符串、换行敏感关键字(如 if 和 ifx)时容易漏匹配或回溯爆炸。手写状态机能精确控制每个字符的流转路径,比如遇到 / 后立刻判断是除号、单行注释还是多行注释起始,避免歧义。
常见错误是把状态设计成“读到某字符就跳转”,却忽略「当前是否在字符串内」「上一个字符是不是转义符」这类上下文。状态必须包含:当前状态码、当前 token 缓冲区、行号、列号、是否刚读完换行。
- 状态枚举建议用
enum class TokenState,别用宏或裸 int,方便调试时打印状态名 - 所有状态转移必须显式覆盖默认分支(
default:),否则未定义行为会静默吞掉非法字符 - 不要在状态中直接 push_back 到最终 token vector——先累积到
std::string current_lexeme,等状态确认为终态再构造Token
如何处理双引号字符串和转义序列
字符串字面量是状态机最容易出错的部分。问题不在引号本身,而在反斜杠:"a"b" 是合法字符串,但 "a\" 是两个反斜杠,"a "(反斜杠+空格)是非法转义。标准做法是:进入字符串状态后,每读一个字符,检查前一个是否为 '\';如果是,就跳过转义逻辑,清空转义标记,把当前字符原样存入;如果不是,再判断当前字符是否为 '"' 或 '\'。
关键细节:换行不能出现在未闭合字符串中(除非用续行符,但简单脚本通常不支持)。所以一旦在 IN_STRING 状态下读到 '
',应立即报错并终止解析。
立即学习“C++免费学习笔记(深入)”;
- 用布尔变量
escaped记录上一字符是否为'\',而不是靠 peek 前一个字符——peek 容易在回退时出错 -
'\'后跟'n'、't'、'r'、'"'、'\'才算合法转义,其余一律视为非法字符 - 字符串结束时,token 的
value字段应存储解码后的字符串(如"\n"→" "),不是原始文本
关键字识别必须放在标识符之后做
如果先查关键字表再判断是否为标识符,会把 ifx 错判成关键字 if + 剩余 x,破坏词法完整性。正确顺序是:先按标识符规则收集成完整单词(只含字母、数字、下划线,且首字符非数字),再查哈希表或 switch-case 判断是否为保留字。
性能影响不大——现代 CPU 分支预测对小表(while 当普通变量名,解析成赋值语句。
- 关键字表用
static const std::unordered_map<std::string, TokenType>初始化一次,别每次解析都重建 - 标识符首字符检查必须严格:
isalpha(c) || c == '_',不能只用isalnum(c),否则数字开头的会被误收 - 遇到
0x或0b开头要进数字状态,不能当标识符——这是常见冲突点
Token 结构体怎么设计才不拖慢解析速度
Token 不是越“完备”越好。很多实现塞了 source_file、full_line_text、column_span 等字段,结果 vector 膨胀 3 倍内存,缓存不友好。实际只需要:类型(TokenType)、原始文本(std::string_view)、行列位置(两个 int)。
std::string_view 是关键:它不拷贝字符,只存指针和长度,前提是源代码缓冲区生命周期长于所有 Token。所以解析函数签名应为 std::vector<Token> tokenize(std::string_view src),而非 std::string。
- 避免在 Token 里存
std::string—— 每次构造都触发堆分配,解析万行脚本可能 malloc 上万次 - 行列号用
int而非size_t:负值可用于标记“未知位置”,且加减运算更安全 - Token 类型用
enum class TokenType : uint8_t,紧凑且可直接 switch,别用 class 封装一堆 getter
最常被忽略的是错误恢复策略:状态机卡死时(比如 unclosed string),不能直接 abort,而要跳到下一个换行符或分号再重启解析——否则整个文件后续 token 全丢。这需要在主循环里加 if (state == ERROR) { skip_to_next_statement(); },而不是抛异常。


















