手把手教你用C++实现一个简易的赋值语句解析器(从词法分析到递归下降语法分析)

张开发
2026/4/15 15:49:51 15 分钟阅读

分享文章

手把手教你用C++实现一个简易的赋值语句解析器(从词法分析到递归下降语法分析)
用C构建赋值语句解析器从词法分析到递归下降语法分析实战编译原理常被视为计算机科学中最抽象的课程之一尤其是当教科书开始讨论LL(1)文法和递归下降分析时许多学习者会感到一头雾水。本文将通过一个具体项目——用C实现赋值语句解析器带你穿透理论迷雾掌握递归下降分析的核心要领。不同于传统教材的数学化描述我们将采用代码先行的方法通过可运行的示例来理解如何分析abc*d这类表达式的语法结构。1. 项目准备与环境搭建在开始编码前我们需要明确几个关键概念和工具准备词法分析与语法分析的关系词法分析器将源代码转换为token流如把a转换为(IDENTIFIER, a)语法分析器则检查这些token是否符合文法规则开发环境配置# 推荐使用g编译Windows可用MinGW g --version # 确认已安装g 9.0或更高版本基础数据结构#include iostream #include vector #include string using namespace std; // Token类型枚举 enum TokenType { IDENTIFIER, // 变量名 OPERATOR, // - * / ASSIGN, // PAREN_LEFT, // ( PAREN_RIGHT,// ) END // 结束标记 }; struct Token { TokenType type; string value; };提示建议将上述代码保存为parser.h头文件后续实现将基于此扩展2. 词法分析器实现词法分析是语法分析的前置步骤我们需要将字符流转换为有意义的token序列。以下是一个简易实现vectorToken tokenize(const string input) { vectorToken tokens; size_t pos 0; while (pos input.length()) { char c input[pos]; if (isspace(c)) { pos; continue; } if (isalpha(c)) { // 标识符 string ident; while (pos input.length() isalnum(input[pos])) { ident input[pos]; } tokens.push_back({IDENTIFIER, ident}); continue; } switch(c) { case : tokens.push_back({ASSIGN, }); break; case : case -: case *: case /: tokens.push_back({OPERATOR, string(1, c)}); break; case (: tokens.push_back({PAREN_LEFT, (}); break; case ): tokens.push_back({PAREN_RIGHT, )}); break; default: cerr 非法字符: c endl; exit(1); } pos; } tokens.push_back({END, }); // 添加结束标记 return tokens; }测试案例void testLexer() { string code a b c * d; auto tokens tokenize(code); for (const auto tok : tokens) { cout Type: tok.type , Value: tok.value endl; } }3. 文法设计与FIRST/FOLLOW集我们采用以下LL(1)文法来描述赋值语句S → V E E → T E E → A T E | ε T → F T T → M F T | ε F → ( E ) | V A → | - M → * | / V → i关键集合计算非终结符FIRST集FOLLOW集S{i}{#}E{(, i}{#, )}E{, -, ε}{#, )}T{(, i}{, -, #, )}T{*, /, ε}{, -, #, )}F{(, i}{*, /, , -, #, )}V{i}{}4. 递归下降分析器实现递归下降分析的核心是为每个非终结符编写一个解析函数。以下是关键部分的实现class Parser { vectorToken tokens; size_t current 0; Token advance() { return tokens[current]; } Token peek() const { return tokens[current]; } bool check(TokenType type) const { return peek().type type; } public: Parser(const vectorToken t) : tokens(t) {} void parse() { S(); if (!check(END)) { error(期望结束符); } cout 语法分析通过! endl; } private: void S() { V(); if (check(ASSIGN)) { advance(); // 消费 E(); } else { error(期望赋值运算符); } } void E() { T(); E_prime(); } void E_prime() { if (check(OPERATOR) (peek().value || peek().value -)) { advance(); // 消费运算符 T(); E_prime(); } // 否则为ε直接返回 } // 其他非终结符函数类似... };错误处理机制void error(const string msg) { cerr 语法错误: msg endl; cerr 当前位置: current endl; exit(1); }5. 测试与调试技巧完善的测试是确保解析器正确的关键。以下是几种典型测试场景正确用例x y za (b c) * d - e / f错误用例及预期输出a b * c→ 报错运算符后应为因子x y→ 缺少表达式a (b c→ 缺少右括号调试输出技巧void E() { cout 进入E endl; T(); E_prime(); cout 离开E endl; }通过这种函数入口/出口的日志可以清晰看到递归调用栈的情况。6. 性能优化与扩展思路基础实现完成后可以考虑以下增强功能错误恢复机制遇到错误时跳过当前token直到同步集合FOLLOW集记录多个错误而非立即退出抽象语法树生成struct ASTNode { string value; vectorASTNode children; }; // 在解析过程中构建AST而非单纯验证运算符优先级处理通过文法设计自然体现优先级*和/比和-有更高优先级这个项目虽然规模不大但涵盖了编译原理的核心概念。当你看到自己编写的解析器成功分析出复杂表达式的结构时那种成就感会让你对理论的理解更加深刻。

更多文章