递归下降更适合词法分析因其天然支持嵌套结构、上下文依赖与状态管理;通过共享游标pos引用实现安全回溯,配合前置越界检查、string_view零拷贝及先数字后标识符的冲突解决策略。

为什么不用正则而选递归下降写词法分析器
正则表达式适合匹配固定模式,但无法处理嵌套结构(比如字符串里的转义、括号配对)和上下文依赖(如0x开头必须跟十六进制数字)。递归下降把词法规则拆成函数,每个函数对应一个语法单元(如parse_number()),天然支持回溯、条件分支和状态携带——这对处理/* */注释、行内//、引号嵌套等场景更直接可控。
实际开发中,你往往不是从零造轮子:而是为 DSL、配置文件或教学目的快速搭个可调试、易扩展的分词器。这时候递归下降比手写状态机更易读,比用 Flex 更轻量。
核心状态管理:如何避免反复扫描和越界
关键不是“递归”,而是共享一个可变的游标pos和输入src。所有解析函数都接受int& pos引用,成功时推进pos,失败时不推进——这是回溯的基础。越界检查必须在每次读取前做,不能只在函数开头检查一次。
- 每次访问
src[pos]前,先判断pos >= src.size(),返回std::nullopt或空Token - 不要用
substr()切片生成临时字符串;直接用src.data() + pos和长度构造std::string_view,避免拷贝 - 跳过空白的逻辑(
skip_whitespace())必须放在每个parse_*入口处,否则123+456会因中间没空格而解析失败
数字与标识符的冲突怎么解
parse_number()和parse_identifier()都从字母/数字开始,顺序一错就全乱。标准做法是:先尝试数字(因为数字格式更严格),再试标识符;如果数字解析中途失败(比如123a),必须把pos还原到起始位置,再交给标识符处理。
立即学习“C++免费学习笔记(深入)”;
示例片段:
Token parse_number(const std::string& src, int& pos) {
int start = pos;
if (src[pos] == '0' && pos + 1 < src.size() && src[pos + 1] == 'x') {
pos += 2; // 跳过 0x
while (isxdigit(src[pos])) ++pos;
return Token{"NUMBER_HEX", src.substr(start, pos - start)};
}
while (isdigit(src[pos])) ++pos;
if (pos == start) return std::nullopt; // 没消耗任何字符,失败
return Token{"NUMBER_DEC", src.substr(start, pos - start)};
}
注意:这里没做小数点或科学计数法支持,是因为一旦加了.,就和标识符(如a.b)产生歧义——真实项目中需根据语言规范决定是否允许,以及是否引入前瞻(lookahead)。
字符串字面量里的转义怎么安全处理
不能简单替换"\n"→"
",因为原始字符串可能含"\"(单个反斜杠)或"\u"(非法Unicode转义)。正确做法是逐字符解析,在parse_string()内部维护一个std::string result,遇到后查表转换:
-
"\n"→' ',"\t"→' ',"\""→'"' -
"\x"后面必须跟两个十六进制数字,否则报错并停止解析(不跳过) - 遇到换行符未闭合引号时,应立即报错,而不是继续读到下一行——否则会掩盖真正的语法错误
最容易被忽略的是:双引号本身要能出现在字符串里,所以"say "hi""必须解析成say "hi",而不是截断成say 。这意味着parse_string()里对"的判断必须排除"的情况。


















