状态机优于正则匹配因能精确控制字节消费与回退、支持嵌套注释等复杂场景且内存可控;需用枚举定义状态、字符分类预处理、二维跳转表,并确保每个状态有明确退出路径及位置记录。

为什么用状态机写词法分析器而不是正则匹配
因为 C++ 标准库的 std::regex 在处理嵌套注释、行延续、字符串转义等边界场景时,要么性能差,要么根本无法表达(比如“匹配直到非转义的引号”)。状态机能精确控制每个字节的消费和回退,且内存占用可控——尤其适合嵌入式或编译器前端这类对确定性要求高的场景。
常见错误是把状态机写成“一堆 if-else 堆砌”,结果漏掉回退逻辑(比如读到 '0' 后发现不是数字而是 '0x',却没把 'x' 放回去),导致后续 Token 错位。正确做法是:每个状态只负责识别当前字符是否合法,并决定下一个状态;仅当确认终结时才提交 Token。
如何设计最小可行的状态跳转表
不需要完整覆盖所有 C++ 语法,先支持:identifier、number(十进制/十六进制)、string_literal(双引号,支持
、")、comment(// 和 /* */)、基础运算符(+ - * / = == != =)和括号。
关键点:
立即学习“C++免费学习笔记(深入)”;
- 用
enum class State { Start, InNumber, InString, InCommentLine, InCommentBlock, ... }显式定义状态,避免 magic number - 输入字符分类预处理:定义
char_class(c)返回Digit/Letter/Quote/Slash/Star/Newline/Other,减少 switch 分支数 - 跳转逻辑写在二维表里:
next_state[State::Start][char_class('/')] = State::MaybeComment,比嵌套 if 更易维护和测试 - 所有状态必须有明确退出路径:比如
State::InString遇到未转义的"→ 提交 Token 并回到State::Start;遇到 EOF → 报错
Token 生成时最容易忽略的三个细节
Token 不只是存类型和原始字符串,还要记录位置信息(行号、列号),否则报错时无法定位。但更隐蔽的问题是:
-
std::string的构造开销:别每次匹配成功都做std::string{begin, end},改用std::string_view持有源缓冲区切片,只在需要 owned 字符串时(如 symbol table 插入)才拷贝 - 数字字面量的进制判断必须在状态中完成:读到
'0'后若下一个是'x'或'X',要进入HexDigits状态,而不是等到整个字符串结束再解析——否则0x123G里的'G'会被当成 identifier 开头 - 注释不能生成 Token,但必须消耗掉换行符:否则
// comment int x;中的会丢失,导致下一行行号错乱
一个可运行的 minimal 示例骨架
下面代码片段展示核心循环结构,不依赖第三方库,仅用标准 C++17:
class Lexer {
const char* ptr_;
int line_ = 1, col_ = 0;
enum class State { Start, InId, InNum, InString, InCommentLine };
State state_ = State::Start;
<p>public:
explicit Lexer(const char* src) : ptr_(src) {}</p><pre class='brush:php;toolbar:false;'>Token next() {
while (true) {
char c = *ptr_;
if (c == ' ') return {TokenType::Eof, "", line_, col_};
if (c == '
') { ++line_; col_ = 0; ++ptr_; continue; }
// 根据 state_ 和 c 跳转,更新 ptr_、line_、col_
// 遇到终结态时返回 Token,例如:
// case State::InString: if (c == '"' && !escaped) { ... return {TokenType::String, sv, line_, col_}; }
}
}};
真正复杂的是状态跳转分支里的字符判断和指针推进逻辑——比如 InString 状态中,遇到 '\' 要标记 escaped = true,下一轮就跳过转义处理;而 InCommentLine 遇到 '
' 必须立即退出并返回 Start,否则会吃掉下一行首字符。
调试时最常卡住的地方是:状态机没处理 EOF 边界(*ptr_ == ' ' 时某些状态直接 crash),或者忘记在跳转前递增 col_ —— 这会导致所有 Token 的列号偏移一位,且极难复现。


















