用C++ 11手撸一个预测分析器:从FIRST/FOLLOW集到完整语法分析(附避坑指南)
2026/6/6 8:40:01 网站建设 项目流程

用C++11实现预测分析器:从理论到实战的完整指南

编译原理课程中最令人头疼的环节之一就是语法分析器的实现。当面对FIRST集、FOLLOW集这些抽象概念时,许多同学都会感到无从下手。本文将带你用C++11一步步构建一个完整的预测分析器,不仅解释核心算法,更重要的是分享如何将这些理论转化为可运行的代码。

1. 环境准备与基础设计

在开始编码前,我们需要明确几个关键点。预测分析器主要适用于LL(1)文法,这类文法要求每个非终结符的各个产生式的FIRST集互不相交,且如果某个产生式能推导出空串,那么它的FIRST集和FOLLOW集也不能有交集。

开发环境配置

  • 编译器:GCC 8.3.0或更高版本
  • 构建工具:CMake(推荐)或直接使用IDE如CLion
  • C++标准:C++11或更高

基础数据结构设计

#include <unordered_set> #include <unordered_map> #include <vector> #include <string> // 产生式规则:左部非终结符 → 右部字符串 using ProductionRule = std::pair<char, std::string>; // 文法定义 struct Grammar { std::unordered_set<char> non_terminals; // 非终结符集合 std::unordered_set<char> terminals; // 终结符集合 std::vector<ProductionRule> rules; // 产生式规则 char epsilon = '$'; // 空字符表示 char start_symbol; // 开始符号 };

2. FIRST集的计算实现

FIRST集的计算是预测分析器的核心之一。我们需要为每个符号(终结符和非终结符)计算其FIRST集。终结符的FIRST集就是它自身,而非终结符的FIRST集则需要通过迭代计算得到。

计算步骤

  1. 初始化所有符号的FIRST集
  2. 对于每个产生式,按照规则更新FIRST集
  3. 重复步骤2直到所有FIRST集不再变化

关键代码实现

using SymbolSet = std::unordered_set<char>; struct FirstFollowSet { SymbolSet first; SymbolSet follow; bool has_epsilon = false; }; using SymbolTable = std::unordered_map<char, FirstFollowSet>; void computeFirstSets(Grammar& grammar, SymbolTable& symbol_table) { bool changed; do { changed = false; for (const auto& rule : grammar.rules) { char lhs = rule.first; const std::string& rhs = rule.second; size_t i = 0; bool all_has_epsilon = true; while (i < rhs.size() && all_has_epsilon) { char symbol = rhs[i]; // 记录原始大小以检测变化 size_t original_size = symbol_table[lhs].first.size(); // 如果是终结符或空字符 if (grammar.terminals.count(symbol) || symbol == grammar.epsilon) { symbol_table[lhs].first.insert(symbol); if (symbol == grammar.epsilon) { symbol_table[lhs].has_epsilon = true; } else { all_has_epsilon = false; } } // 如果是非终结符 else { // 将非终结符的FIRST集(除ε外)加入 for (char ch : symbol_table[symbol].first) { if (ch != grammar.epsilon) { symbol_table[lhs].first.insert(ch); } } // 如果该非终结符的FIRST集包含ε,继续检查下一个符号 if (!symbol_table[symbol].has_epsilon) { all_has_epsilon = false; } } // 检查是否发生了变化 if (symbol_table[lhs].first.size() > original_size) { changed = true; } i++; } // 如果所有符号都能推导出ε,则将ε加入FIRST集 if (all_has_epsilon && !symbol_table[lhs].has_epsilon) { symbol_table[lhs].has_epsilon = true; symbol_table[lhs].first.insert(grammar.epsilon); changed = true; } } } while (changed); }

常见陷阱

  • 忘记处理产生式右部所有符号都能推导出空串的情况
  • 在迭代过程中没有正确判断何时停止检查后续符号
  • 没有正确处理终结符和空字符的特殊情况

3. FOLLOW集的计算技巧

FOLLOW集的计算比FIRST集更为复杂,因为它需要考虑产生式右部符号之间的关系以及递归依赖。计算时需要特别注意顺序,通常需要多次遍历所有产生式直到结果稳定。

计算算法要点

  1. 将结束符'#'加入开始符号的FOLLOW集
  2. 对于每个产生式A→αBβ:
    • 将FIRST(β)中除ε外的元素加入FOLLOW(B)
    • 如果β能推导出ε,将FOLLOW(A)加入FOLLOW(B)
  3. 对于产生式A→αB,将FOLLOW(A)加入FOLLOW(B)

实现代码

void computeFollowSets(Grammar& grammar, SymbolTable& symbol_table) { // 初始化开始符号的FOLLOW集 symbol_table[grammar.start_symbol].follow.insert('#'); bool changed; do { changed = false; for (const auto& rule : grammar.rules) { char lhs = rule.first; const std::string& rhs = rule.second; for (size_t i = 0; i < rhs.size(); ++i) { char B = rhs[i]; // 只处理非终结符 if (grammar.non_terminals.count(B) == 0) continue; // 检查B后面是否有符号 if (i + 1 < rhs.size()) { char beta = rhs[i+1]; // 将FIRST(beta)中除ε外的元素加入FOLLOW(B) size_t original_size = symbol_table[B].follow.size(); if (grammar.terminals.count(beta)) { // beta是终结符 symbol_table[B].follow.insert(beta); } else { // beta是非终结符,加入其FIRST集(除ε外) for (char ch : symbol_table[beta].first) { if (ch != grammar.epsilon) { symbol_table[B].follow.insert(ch); } } } // 如果beta能推导出ε,需要将FOLLOW(A)加入FOLLOW(B) bool beta_has_epsilon = grammar.terminals.count(beta) ? false : symbol_table[beta].has_epsilon; if (beta_has_epsilon) { for (char ch : symbol_table[lhs].follow) { symbol_table[B].follow.insert(ch); } } if (symbol_table[B].follow.size() > original_size) { changed = true; } } // B是产生式最后一个符号,将FOLLOW(A)加入FOLLOW(B) else { size_t original_size = symbol_table[B].follow.size(); for (char ch : symbol_table[lhs].follow) { symbol_table[B].follow.insert(ch); } if (symbol_table[B].follow.size() > original_size) { changed = true; } } } } } while (changed); }

调试技巧

  • 在每次迭代后打印FOLLOW集的变化,便于发现问题
  • 特别注意那些FOLLOW集应该包含结束符'#'的非终结符
  • 检查是否有非终结符的FOLLOW集始终为空,这通常意味着计算有误

4. 预测分析表的构建策略

有了FIRST集和FOLLOW集后,我们就可以构建预测分析表了。预测分析表是一个二维表格,行对应非终结符,列对应终结符(包括结束符'#'),每个单元格指示应该使用哪个产生式。

表格构建规则

  1. 对于每个产生式A→α:
    • 对于FIRST(α)中的每个终结符a(不包括ε),将A→α加入M[A,a]
    • 如果ε在FIRST(α)中,对于FOLLOW(A)中的每个终结符b(包括#),将A→α加入M[A,b]
  2. 如果任何表项有多个产生式,则文法不是LL(1)文法

数据结构选择: 由于非终结符和终结符都是字符,我们可以使用压缩技术将两个字符组合成一个整数作为哈希键。

#include <cstdint> using PredictTable = std::unordered_map<uint16_t, int>; uint16_t makeKey(char non_terminal, char terminal) { return (static_cast<uint16_t>(non_terminal) << 8) | static_cast<uint16_t>(terminal); } void buildPredictTable(const Grammar& grammar, const SymbolTable& symbol_table, PredictTable& predict_table) { int rule_index = 0; for (const auto& rule : grammar.rules) { char A = rule.first; const std::string& alpha = rule.second; // 计算FIRST(alpha) SymbolSet first_alpha; bool alpha_has_epsilon = true; for (char X : alpha) { if (grammar.terminals.count(X)) { first_alpha.insert(X); alpha_has_epsilon = false; break; } else if (X == grammar.epsilon) { first_alpha.insert(grammar.epsilon); break; } else { // 加入FIRST(X)中除ε外的符号 for (char a : symbol_table.at(X).first) { if (a != grammar.epsilon) { first_alpha.insert(a); } } // 如果FIRST(X)不包含ε,停止 if (!symbol_table.at(X).has_epsilon) { alpha_has_epsilon = false; break; } } } // 对于FIRST(alpha)中的每个终结符a,添加表项 for (char a : first_alpha) { if (a != grammar.epsilon) { uint16_t key = makeKey(A, a); if (predict_table.count(key) && predict_table[key] != rule_index) { std::cerr << "Conflict in predict table: M[" << A << "," << a << "] has multiple rules" << std::endl; } predict_table[key] = rule_index; } } // 如果α能推导出ε,对于FOLLOW(A)中的每个终结符b,添加表项 if (alpha_has_epsilon || alpha.find(grammar.epsilon) != std::string::npos) { for (char b : symbol_table.at(A).follow) { uint16_t key = makeKey(A, b); if (predict_table.count(key) && predict_table[key] != rule_index) { std::cerr << "Conflict in predict table: M[" << A << "," << b << "] has multiple rules" << std::endl; } predict_table[key] = rule_index; } } rule_index++; } }

优化技巧

  • 使用位运算压缩字符对为整数键,提高哈希表效率
  • 在构建过程中检测并报告冲突,帮助发现文法问题
  • 可以为常用终结符预先计算哈希值,减少运行时计算

5. 总控程序的实现细节

总控程序是预测分析器的执行引擎,它使用分析栈和输入缓冲区按照预测分析表进行推导。实现时需要注意错误处理和状态跟踪。

算法流程

  1. 初始化栈为开始符号和结束符'#'
  2. 读取第一个输入符号
  3. 循环直到栈为空:
    • 如果栈顶是终结符且匹配输入,弹出栈顶并消费输入
    • 如果栈顶是非终结符,查表选择产生式进行推导
    • 如果出现不匹配或表项为空,报告语法错误

核心实现

#include <stack> #include <iostream> void predictiveParser(Grammar& grammar, const PredictTable& predict_table, const std::string& input) { std::stack<char> parse_stack; parse_stack.push('#'); parse_stack.push(grammar.start_symbol); size_t input_pos = 0; char current_input = input.empty() ? '#' : input[input_pos]; std::cout << "Step\tStack\t\tInput\t\tAction" << std::endl; int step = 0; while (!parse_stack.empty()) { char X = parse_stack.top(); // 打印当前状态 std::cout << step++ << "\t"; printStack(parse_stack); std::cout << "\t"; printInput(input, input_pos); std::cout << "\t"; // 栈顶是终结符或# if (X == current_input) { parse_stack.pop(); if (current_input != '#') { input_pos++; current_input = input_pos < input.size() ? input[input_pos] : '#'; } std::cout << "Match '" << X << "'" << std::endl; } else if (grammar.terminals.count(X) || X == '#') { std::cout << "Error: expecting '" << X << "' but found '" << current_input << "'" << std::endl; return; } // 栈顶是非终结符 else { uint16_t key = makeKey(X, current_input); if (predict_table.count(key)) { int rule_idx = predict_table.at(key); const auto& rule = grammar.rules[rule_idx]; parse_stack.pop(); // 将产生式右部逆序压栈 if (rule.second != "$") { // 如果不是空产生式 for (auto it = rule.second.rbegin(); it != rule.second.rend(); ++it) { parse_stack.push(*it); } } std::cout << "Apply " << rule.first << " -> " << rule.second << std::endl; } else { std::cout << "Error: no production for " << X << " on '" << current_input << "'" << std::endl; return; } } } if (current_input == '#') { std::cout << "Parse successful!" << std::endl; } else { std::cout << "Error: extra input after parse" << std::endl; } }

辅助函数

void printStack(const std::stack<char>& s) { std::stack<char> temp = s; while (!temp.empty()) { std::cout << temp.top(); temp.pop(); } } void printInput(const std::string& input, size_t pos) { if (pos >= input.size()) { std::cout << "#"; } else { std::cout << input.substr(pos); } }

错误处理建议

  • 提供详细的错误信息,包括位置和预期符号
  • 考虑实现错误恢复机制,如恐慌模式或短语级恢复
  • 可以添加语法错误收集功���,一次报告所有错误而非遇到第一个错误就停止

6. 测试与调试实战经验

完成预测分析器实现后,测试是确保其正确性的关键环节。好的测试案例应该覆盖各种边界情况和可能的错误模式。

测试文法示例

E T A F B + * ( ) i E TA A +TA A $ T FB B *FB B $ F (E) F i

测试案例设计

  1. 合法输入测试:

    • i+i*i(简单表达式)
    • (i+i)*i(带括号)
    • i(最小合法输入)
  2. 错误输入测试:

    • i+(不完整表达式)
    • i++i(连续运算符)
    • (i(不匹配的括号)
    • i*i+i)多余的右括号

调试输出示例

Step Stack Input Action 0 #E i+i*i# Apply E -> TA 1 #AT i+i*i# Apply T -> FB 2 #ABF i+i*i# Apply F -> i 3 #ABi i+i*i# Match 'i' 4 #AB +i*i# Apply B -> $ 5 #A +i*i# Apply A -> +TA 6 #AT+ +i*i# Match '+' 7 #AT i*i# Apply T -> FB 8 #ABF i*i# Apply F -> i 9 #ABi i*i# Match 'i' 10 #AB *i# Apply B -> *FB 11 #ABF* *i# Match '*' 12 #ABF i# Apply F -> i 13 #ABi i# Match 'i' 14 #AB # Apply B -> $ 15 #A # Apply A -> $ 16 # # Match '#' Parse successful!

性能优化建议

  • 对于大型文法,考虑使用更高效的数据结构如std::bitset表示集合
  • 缓存中间计算结果,避免重复计算
  • 对于固定文法,可以预计算分析表并序列化保存

7. 高级话题与扩展思路

掌握了基础实现后,可以考虑以下几个方向的扩展:

错误恢复机制

  • 恐慌模式:发现错误时跳过输入直到同步符号
  • 短语级恢复:在错误位置插入/删除/替换符号
  • 错误产生式:为常见错误添加特殊产生式

可视化工具

  • 实现分析过程的图形化展示
  • 生成分析树或推导过程的动画
  • 交互式单步执行调试

文法分析增强

  • 自动检测文法是否为LL(1)
  • 对非LL(1)文法提出转换建议
  • 文法优化建议(消除左递归、提取左公因子)

与其他组件集成

  • 与词法分析器无缝衔接
  • 生成抽象语法树(AST)作为输出
  • 添加简单的语义动作

实现一个完整的预测分析器是理解编译原理的重要一步。通过这个项目,你不仅掌握了LL(1)分析的核心算法,还学会了如何将复杂理论转化为实际代码。当看到自己实现的解析器能够正确分析各种输入时,那种成就感会让你觉得所有的努力都是值得的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询