用C++手把手实现算符优先分析器:从FIRSTVT/LASTVT到移进归约的完整流程
在编译原理的语法分析阶段,算符优先分析法因其直观性和高效性成为处理表达式解析的经典方法。本文将带您用C++从零构建完整的算符优先分析器,重点解决三个核心问题:如何自动生成FIRSTVT/LASTVT集合、如何构造优先关系表,以及如何实现移进-归约的解析流程。不同于理论教材的抽象描述,我们将通过200+行可运行代码演示每个环节的具体实现。
1. 理解算符优先分析法的核心机制
算符优先分析法的本质是通过比较相邻运算符的优先级来决定语法分析的动作。其核心在于建立三种优先关系:
a < b:b的优先级高于aa = b:两者优先级相等a > b:a的优先级高于b
实现时需要特别注意两个特殊处理:
- 非终结符(如E、T等)不参与优先比较
- 遇到产生式右部含有非终结符时,需要借助FIRSTVT和LASTVT集合推导优先级
典型表达式文法的优先关系表示例:
| + | * | i | ( | ) | # | |
|---|---|---|---|---|---|---|
| + | > | < | < | < | > | > |
| * | > | > | < | < | > | > |
| i | > | > | > | > | ||
| ( | < | < | < | < | = | |
| ) | > | > | > | > | ||
| # | < | < | < | < | = |
2. 构建FIRSTVT与LASTVT集合
2.1 数据结构设计
我们使用二维数组存储文法规则和计算结果:
string V[100][2]; // 存储拆分后的产生式 string FIRSTVT[20][2]; // 每个非终结符的FIRSTVT集合 string LASTVT[20][2]; // 每个非终结符的LASTVT集合2.2 FIRSTVT集合计算算法实现
FIRSTVT(P)定义为可从P推导出的首个终结符,实现时需要递归处理:
void add_firstvt(string b, int a) { for(char c : b) { if(isupper(c)) continue; // 跳过非终结符 if(FIRSTVT[a][1].find(c) == string::npos) { FIRSTVT[a][1] += c; } } } string get_cur_firstvt(char non_terminal, int index) { for(int i=0; i<vi; i++) { if(V[i][0][0] == non_terminal) { char first_char = V[i][1][0]; if(!isupper(first_char)) { // 直接终结符 add_firstvt(string(1,first_char), index); } else { // 非终结符情况 if(first_char != non_terminal) { // 避免直接左递归 get_cur_firstvt(first_char, find_index(first_char)); } // 处理第二个字符(如果有) if(V[i][1].length()>1 && !isupper(V[i][1][1])) { add_firstvt(string(1,V[i][1][1]), index); } } } } return FIRSTVT[index][1]; }2.3 LASTVT集合的计算要点
LASTVT的计算与FIRSTVT对称,但需要从产生式右部末尾向前扫描。关键代码差异:
// 在get_cur_lastvt中改为检查产生式末尾字符 if(!isupper(V[i][1].back())) { add_lastvt(string(1,V[i][1].back()), index); }3. 构造优先关系表
3.1 表结构初始化
使用二维字符数组存储优先关系:
char table[100][100]; // 优先关系表 void init_table() { // 收集所有终结符 set<char> terminals; for(int i=0; i<vi; i++) { for(char c : V[i][1]) { if(!isupper(c) && c!='|') terminals.insert(c); } } terminals.insert('#'); // 添加边界符 // 初始化表头 int col = 1; for(char t : terminals) { table[0][col] = t; table[col][0] = t; col++; } }3.2 关系填充算法
根据文法规则填充三种优先关系:
void fill_relations() { for(int i=0; i<vi; i++) { string rhs = V[i][1]; for(int j=0; j<rhs.size()-1; j++) { // 情况1:直接相邻的终结符 a=b if(!isupper(rhs[j]) && !isupper(rhs[j+1])) { set_relation(rhs[j], rhs[j+1], '='); } // 情况2:aQb形式 if(j<rhs.size()-2 && !isupper(rhs[j]) && isupper(rhs[j+1]) && !isupper(rhs[j+2])) { set_relation(rhs[j], rhs[j+2], '='); } // 情况3:aQ形式,a<FIRSTVT(Q) if(!isupper(rhs[j]) && isupper(rhs[j+1])) { int q_idx = find_index(rhs[j+1]); for(char f : FIRSTVT[q_idx][1]) { set_relation(rhs[j], f, '<'); } } // 情况4:Qa形式,LASTVT(Q)>a if(isupper(rhs[j]) && !isupper(rhs[j+1])) { int q_idx = find_index(rhs[j]); for(char l : LASTVT[q_idx][1]) { set_relation(l, rhs[j+1], '>'); } } } } }4. 移进-归约分析器实现
4.1 核心数据结构
stack<char> analysis_stack; // 分析栈 string input_buffer; // 输入缓冲区 vector<vector<string>> analysis_steps; // 记录分析过程4.2 分析流程控制
void analyze() { analysis_stack.push('#'); input_buffer += '#'; while(true) { char stack_top = get_stack_top_terminal(); char input_front = input_buffer.front(); char relation = get_relation(stack_top, input_front); if(relation == '<' || relation == '=') { // 移进动作 analysis_stack.push(input_front); input_buffer.erase(0,1); record_step("移进"); } else if(relation == '>') { // 归约动作 if(!reduce()) { error("无法归约"); break; } record_step("归约"); } else if(stack_top == '#' && input_front == '#') { record_step("接受"); break; } else { error("语法错误"); break; } } }4.3 归约处理的关键实现
bool reduce() { // 从栈顶寻找可归约的串 string handle; vector<char> temp; // 找出最左素短语 do { char c = analysis_stack.top(); temp.push_back(c); analysis_stack.pop(); } while(!is_reducible(temp)); // 反转得到正确的handle顺序 reverse(temp.begin(), temp.end()); handle = string(temp.begin(), temp.end()); // 查找匹配的产生式 for(int i=0; i<vi; i++) { if(can_reduce(handle, V[i][1])) { analysis_stack.push(V[i][0][0]); return true; } } return false; }5. 调试技巧与常见问题解决
在实际编码过程中,有几个容易出错的点需要特别注意:
优先关系表不完整
- 检查是否遗漏了边界符号'#'的关系
- 确认所有终结符都参与了关系计算
归约失败问题
- 打印分析栈和剩余输入帮助调试
void debug_print() { cout << "栈: "; stack<char> temp = analysis_stack; while(!temp.empty()) { cout << temp.top() << " "; temp.pop(); } cout << "\n输入: " << input_buffer << endl; }FIRSTVT/LASTVT计算错误
- 添加调试输出验证中间结果
- 特别注意递归产生式的处理
多字符运算符处理
- 扩展词法分析器返回复合token
- 修改优先关系表处理多字符运算符
一个实用的调试技巧是在每个分析步骤打印当前状态:
| 步骤 | 分析栈 | 优先关系 | 剩余输入 | 动作 |
|---|---|---|---|---|
| 1 | # | < | i+i*i# | 移进 |
| 2 | #i | > | +i*i# | 归约 E |
| 3 | #E | < | +i*i# | 移进 |
6. 完整代码结构与测试案例
项目建议采用以下模块化结构:
operator_precedence/ ├── include/ │ ├── grammar.h // 文法定义 │ ├── analyzer.h // 分析器接口 ├── src/ │ ├── firstvt.cpp // FIRSTVT计算 │ ├── lastvt.cpp // LASTVT计算 │ ├── table.cpp // 优先表构建 │ ├── analyze.cpp // 移进-归约实现 └── test/ ├── test_grammar.txt ├── test_cases/ ├── arithmetic.txt ├── logic.txt测试文法示例:
E -> E+T | T T -> T*F | F F -> (E) | i测试输入表达式:
i+i*(i+i)#在实现过程中,建议先使用小规模文法验证各模块正确性,再逐步扩展复杂文法。对于更复杂的语言结构,可以考虑扩展为更通用的优先分析法。