从FIRST/FOLLOW到预测分析表:LL(1)文法C++实现核心代码解析

发布时间:2026/11/4 21:07:27

从FIRST/FOLLOW到预测分析表:LL(1)文法C++实现核心代码解析 从FIRST/FOLLOW到预测分析表LL(1)文法C实现核心代码解析在编译器构建的领域中语法分析器扮演着至关重要的角色。它如同一位严谨的语法检察官负责检查源代码是否符合编程语言的语法规则。而LL(1)分析器作为自顶向下分析的代表以其高效和直观的特点成为许多编译器前端的选择。本文将深入探讨如何用C实现LL(1)文法的核心算法从基础集合计算到预测分析表的构建最终完成一个可运行的语法分析器。1. 文法表示与数据结构设计实现LL(1)分析器的第一步是设计合适的数据结构来表示文法。在C中我们可以利用STL容器来高效地存储和操作文法规则。#include iostream #include vector #include unordered_map #include unordered_set #include string using namespace std; // 文法产生式结构体 struct Production { char left; // 左部非终结符 string right; // 右部符号串 Production(char l, string r) : left(l), right(r) {} }; class Grammar { private: vectorProduction productions; // 所有产生式 unordered_setchar nonTerminals; // 非终结符集合 unordered_setchar terminals; // 终结符集合 char startSymbol; // 开始符号 public: // 添加产生式 void addProduction(char left, string right) { productions.emplace_back(left, right); nonTerminals.insert(left); for (char c : right) { if (isupper(c)) nonTerminals.insert(c); else if (c ! ε) terminals.insert(c); } } // 设置开始符号 void setStartSymbol(char start) { startSymbol start; } // 获取所有产生式 const vectorProduction getProductions() const { return productions; } // 获取非终结符集合 const unordered_setchar getNonTerminals() const { return nonTerminals; } // 获取终结符集合 const unordered_setchar getTerminals() const { return terminals; } // 获取开始符号 char getStartSymbol() const { return startSymbol; } };这个Grammar类提供了文法的基本表示包括产生式存储使用vector存储所有产生式符号分类用unordered_set分别存储非终结符和终结符开始符号标记明确文法的入口点提示在实际应用中ε(空串)通常用特殊符号表示这里我们使用ε字符。注意在计算时需要特别处理这种情况。2. FIRST集合的计算算法FIRST集合是LL(1)分析的基础它告诉我们每个非终结符可能推导出的首个终结符是什么。以下是计算FIRST集合的核心代码class LL1Analyzer { private: Grammar grammar; unordered_mapchar, unordered_setchar firstSets; unordered_mapchar, bool nullable; // 计算哪些非终结符可以推导出空串 void calculateNullable() { bool changed; do { changed false; for (const auto prod : grammar.getProductions()) { if (prod.right ε) { if (!nullable[prod.left]) { nullable[prod.left] true; changed true; } continue; } bool allNullable true; for (char c : prod.right) { if (!isupper(c) || !nullable[c]) { allNullable false; break; } } if (allNullable !nullable[prod.left]) { nullable[prod.left] true; changed true; } } } while (changed); } // 计算FIRST集合 void calculateFirstSets() { // 初始化终结符的FIRST集合 for (char c : grammar.getTerminals()) { firstSets[c].insert(c); } bool changed; do { changed false; for (const auto prod : grammar.getProductions()) { char left prod.left; const string right prod.right; if (right ε) continue; size_t oldSize firstSets[left].size(); // 处理产生式右部的每个符号 bool breakLoop false; for (char c : right) { // 如果是终结符直接加入并停止 if (!isupper(c)) { firstSets[left].insert(c); breakLoop true; break; } // 加入非终结符的FIRST集(除ε外) for (char terminal : firstSets[c]) { if (terminal ! ε) { firstSets[left].insert(terminal); } } // 如果当前符号不能推导出ε停止 if (!nullable[c]) { breakLoop true; break; } } // 如果所有符号都能推导出ε加入ε if (!breakLoop) { firstSets[left].insert(ε); } if (firstSets[left].size() ! oldSize) { changed true; } } } while (changed); } public: LL1Analyzer(const Grammar g) : grammar(g) { // 初始化nullable表 for (char nt : grammar.getNonTerminals()) { nullable[nt] false; } calculateNullable(); calculateFirstSets(); } // 获取FIRST集合 const unordered_mapchar, unordered_setchar getFirstSets() const { return firstSets; } // 获取nullable表 const unordered_mapchar, bool getNullable() const { return nullable; } };这段代码实现了两个关键算法calculateNullable()确定哪些非终结符可以推导出空串calculateFirstSets()计算所有符号的FIRST集合算法采用迭代方式直到集合不再变化为止。对于产生式A → BCDFIRST(A)包含FIRST(B)中所有非ε符号如果B可推导出ε则继续加入FIRST(C)的非ε符号以此类推如果所有符号都可推导出ε则加入ε3. FOLLOW集合的计算方法FOLLOW集合告诉我们哪些终结符可能出现在某个非终结符的后面。以下是计算FOLLOW集合的实现class LL1Analyzer { // ... 前面的代码不变 private: unordered_mapchar, unordered_setchar followSets; // 计算FOLLOW集合 void calculateFollowSets() { // 初始化开始符号加入结束符$ followSets[grammar.getStartSymbol()].insert($); bool changed; do { changed false; for (const auto prod : grammar.getProductions()) { const string right prod.right; for (size_t i 0; i right.size(); i) { char current right[i]; if (!isupper(current)) continue; // 只处理非终结符 size_t oldSize followSets[current].size(); // 情况1A → αBβ将FIRST(β)-{ε}加入FOLLOW(B) if (i 1 right.size()) { char next right[i1]; if (!isupper(next)) { // β是终结符 followSets[current].insert(next); } else { // β是非终结符 for (char c : firstSets.at(next)) { if (c ! ε) { followSets[current].insert(c); } } // 如果β可推导出ε还需要加入FOLLOW(A) if (nullable.at(next)) { for (char c : followSets.at(prod.left)) { followSets[current].insert(c); } } } } // 情况2A → αB将FOLLOW(A)加入FOLLOW(B) else { for (char c : followSets.at(prod.left)) { followSets[current].insert(c); } } if (followSets[current].size() ! oldSize) { changed true; } } } } while (changed); } public: LL1Analyzer(const Grammar g) : grammar(g) { // ... 前面的初始化代码 calculateNullable(); calculateFirstSets(); calculateFollowSets(); // 新增 } // 获取FOLLOW集合 const unordered_mapchar, unordered_setchar getFollowSets() const { return followSets; } };FOLLOW集合的计算需要考虑两种主要情况非终结符后有符号将后面符号的FIRST集(除ε外)加入当前非终结符的FOLLOW集非终结符在末尾将产生式左部的FOLLOW集加入当前非终结符的FOLLOW集算法同样采用迭代方式直到所有FOLLOW集不再变化。4. SELECT集合与预测分析表构建SELECT集合结合了FIRST和FOLLOW集合用于确定在特定情况下应该使用哪个产生式。以下是实现代码class LL1Analyzer { // ... 前面的代码不变 private: unordered_mapint, unordered_setchar selectSets; unordered_mapchar, unordered_mapchar, int parsingTable; // 计算SELECT集合 void calculateSelectSets() { for (size_t i 0; i grammar.getProductions().size(); i) { const auto prod grammar.getProductions()[i]; unordered_setchar firstOfRight; bool allNullable true; // 计算产生式右部的FIRST集 for (char c : prod.right) { if (c ε) break; if (!isupper(c)) { // 终结符 firstOfRight.insert(c); allNullable false; break; } // 加入非终结符的FIRST集(除ε外) for (char terminal : firstSets.at(c)) { if (terminal ! ε) { firstOfRight.insert(terminal); } } if (!nullable.at(c)) { allNullable false; break; } } if (allNullable || prod.right ε) { // SELECT(A→α) (FIRST(α)-{ε}) ∪ FOLLOW(A) for (char c : firstOfRight) { selectSets[i].insert(c); } for (char c : followSets.at(prod.left)) { selectSets[i].insert(c); } } else { // SELECT(A→α) FIRST(α) selectSets[i] firstOfRight; } } } // 构建预测分析表 void buildParsingTable() { for (const auto entry : selectSets) { int prodIndex entry.first; const auto selectSet entry.second; char left grammar.getProductions()[prodIndex].left; for (char terminal : selectSet) { // 检查冲突 if (parsingTable[left].count(terminal) parsingTable[left][terminal] ! prodIndex) { cerr 文法不是LL(1)文法存在冲突 endl; return; } parsingTable[left][terminal] prodIndex; } } } public: LL1Analyzer(const Grammar g) : grammar(g) { // ... 前面的初始化代码 calculateNullable(); calculateFirstSets(); calculateFollowSets(); calculateSelectSets(); buildParsingTable(); } // 获取预测分析表 const unordered_mapchar, unordered_mapchar, int getParsingTable() const { return parsingTable; } // 打印预测分析表 void printParsingTable() const { cout 预测分析表 endl; cout 非终结符\\终结符\t; // 收集所有终结符(包括$) unordered_setchar allTerminals grammar.getTerminals(); allTerminals.insert($); // 打印表头 for (char t : allTerminals) { cout t \t; } cout endl; // 打印表内容 for (char nt : grammar.getNonTerminals()) { cout nt \t\t; for (char t : allTerminals) { if (parsingTable.at(nt).count(t)) { int prodIndex parsingTable.at(nt).at(t); const auto prod grammar.getProductions()[prodIndex]; cout prod.left → prod.right \t; } else { cout \t; } } cout endl; } } };SELECT集合的计算规则对于产生式A→α如果α不能推导出ε则SELECT(A→α) FIRST(α)如果α可以推导出ε则SELECT(A→α) (FIRST(α)-{ε}) ∪ FOLLOW(A)预测分析表的构建则是将每个产生式根据其SELECT集合放入相应的表项中。如果发现冲突(同一表项有多个产生式)则说明文法不是LL(1)文法。5. 语法分析器的实现有了预测分析表我们就可以实现语法分析器了。以下是基于栈的LL(1)分析器实现class Parser { private: const Grammar grammar; const unordered_mapchar, unordered_mapchar, int parsingTable; public: Parser(const Grammar g, const unordered_mapchar, unordered_mapchar, int table) : grammar(g), parsingTable(table) {} // 分析输入字符串 bool parse(const string input) { stackchar stk; stk.push($); stk.push(grammar.getStartSymbol()); size_t inputPos 0; char lookahead inputPos input.size() ? input[inputPos] : $; cout 分析过程 endl; printStep(stk, input, inputPos); while (!stk.empty()) { char top stk.top(); if (top $) { if (lookahead $) return true; else break; } if (!isupper(top)) { // 终结符 if (top lookahead) { stk.pop(); inputPos; lookahead inputPos input.size() ? input[inputPos] : $; } else { break; } } else { // 非终结符 if (parsingTable.at(top).count(lookahead)) { int prodIndex parsingTable.at(top).at(lookahead); const auto prod grammar.getProductions()[prodIndex]; stk.pop(); if (prod.right ! ε) { for (auto it prod.right.rbegin(); it ! prod.right.rend(); it) { stk.push(*it); } } } else { break; } } printStep(stk, input, inputPos); } return false; } private: // 打印分析步骤 void printStep(const stackchar stk, const string input, size_t pos) { stackchar temp stk; string stackStr; while (!temp.empty()) { stackStr temp.top(); temp.pop(); } string inputStr input.substr(pos); if (inputStr.empty()) inputStr $; cout 栈: stackStr \t输入: inputStr endl; } };分析器的工作流程初始化栈压入开始符号和结束符$读取输入的下一个字符作为lookahead根据栈顶元素和lookahead决定下一步操作栈顶是终结符匹配则弹出否则报错栈顶是非终结符查预测分析表用对应产生式右部替换栈顶重复直到栈为空或发现错误6. 完整示例与测试让我们用一个完整的例子来测试我们的实现int main() { // 定义文法E → TE // E → TE | ε // T → FT // T → *FT | ε // F → (E) | id Grammar g; g.setStartSymbol(E); g.addProduction(E, TX); g.addProduction(X, TX); g.addProduction(X, ε); g.addProduction(T, FY); g.addProduction(Y, *FY); g.addProduction(Y, ε); g.addProduction(F, (E)); g.addProduction(F, id); // 构建LL(1)分析器 LL1Analyzer analyzer(g); // 打印FIRST和FOLLOW集合 cout FIRST集合 endl; for (const auto entry : analyzer.getFirstSets()) { cout entry.first : {; for (char c : entry.second) { cout c ; } cout } endl; } cout \nFOLLOW集合 endl; for (const auto entry : analyzer.getFollowSets()) { cout entry.first : {; for (char c : entry.second) { cout c ; } cout } endl; } // 打印预测分析表 analyzer.printParsingTable(); // 测试分析器 Parser parser(g, analyzer.getParsingTable()); string input1 idid*id; cout \n分析输入: input1 endl; bool result1 parser.parse(input1); cout (result1 ? 接受 : 拒绝) endl; string input2 id*id; cout \n分析输入: input2 endl; bool result2 parser.parse(input2); cout (result2 ? 接受 : 拒绝) endl; return 0; }这个示例展示了如何定义算术表达式的LL(1)文法计算并打印FIRST和FOLLOW集合构建和显示预测分析表测试语法分析器对合法和非法输入的分析7. 工程实践中的优化与扩展在实际编译器项目中LL(1)分析器的实现还可以进行以下优化和扩展错误恢复当遇到语法错误时可以尝试跳过错误部分并继续分析更高效的数据结构对于大型文法可以使用更紧凑的数据结构存储分析表自动文法转换实现左递归消除和左公因子提取的自动化语义动作在分析过程中嵌入语义分析代码生成抽象语法树在规约时构建语法树而非仅仅验证语法以下是一个简单的错误恢复实现示例bool Parser::parseWithRecovery(const string input) { stackchar stk; stk.push($); stk.push(grammar.getStartSymbol()); size_t inputPos 0; char lookahead inputPos input.size() ? input[inputPos] : $; bool hasError false; while (!stk.empty()) { char top stk.top(); if (top $) { if (lookahead $) return !hasError; else { // 错误输入未结束但栈已空 cerr 错误输入未结束 endl; inputPos; lookahead inputPos input.size() ? input[inputPos] : $; hasError true; continue; } } if (!isupper(top)) { // 终结符 if (top lookahead) { stk.pop(); inputPos; lookahead inputPos input.size() ? input[inputPos] : $; } else { // 错误不匹配的终结符 cerr 错误期望 top 但找到 lookahead endl; inputPos; lookahead inputPos input.size() ? input[inputPos] : $; hasError true; } } else { // 非终结符 if (parsingTable.at(top).count(lookahead)) { int prodIndex parsingTable.at(top).at(lookahead); const auto prod grammar.getProductions()[prodIndex]; stk.pop(); if (prod.right ! ε) { for (auto it prod.right.rbegin(); it ! prod.right.rend(); it) { stk.push(*it); } } } else { // 错误无可用产生式 cerr 错误非终结符 top 无法处理输入 lookahead endl; inputPos; lookahead inputPos input.size() ? input[inputPos] : $; hasError true; } } } return !hasError; }这个改进版本在遇到错误时会输出错误信息跳过当前输入符号并继续分析最后返回是否发现错误。
延伸阅读

更多相关文章

2026/11/5 5:34:15

Git 2.44.0 Windows 11 安装:3个关键配置项详解与SSH密钥生成验证

Git 2.44.0 Windows 11 安装:3个关键配置项详解与SSH密钥生成验证Windows 11 开发者经常面临一个尴尬场景:明明按照教程一步步安装Git,却在团队协作时遭遇换行符冲突、提交记录显示陌生用户名、或是SSH连接频繁失败。这些看似琐碎的问题往往源…

2026/11/2 17:59:59

基于Bluetooth 5.4 LE Audio的无线音频传输方案设计与实现

1. 项目背景与核心组件选型在无线音频传输领域,Bluetooth 5.4标准带来的LE Audio特性正在重塑行业格局。这个项目基于IDC777-1蓝牙模块和PIC32MZ2048EFH144微控制器构建了一套完整的无线音频串流解决方案。IDC777-1是一款支持双模(Classic Bluetooth和BL…

2026/11/1 22:42:13

LL(1)文法实战:3步完成SELECT集求解与预测分析表构建

LL(1)文法实战:3步完成SELECT集求解与预测分析表构建1. 理解LL(1)分析的核心逻辑当你第一次接触LL(1)文法时,可能会被各种集合计算绕晕。让我们换个角度思考:这其实是一个"决策树"构建过程。想象你正在编写一个智能解析器&#xff…

2026/11/5 15:00:49

基于Hadoop+Spark的民宿智能推荐系统设计与实现

1. 项目概述:大数据时代的民宿智能推荐这个毕业设计项目融合了当前大数据领域最主流的技术栈,构建了一个完整的民宿推荐系统解决方案。作为一名长期从事大数据系统开发的工程师,我认为这个选题非常贴合实际应用场景——随着旅游业的复苏和共享…

2026/11/5 14:10:30

ADS122U04IPWR+STM32F427ZI工业级温度测量信号链设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配RS485,解决远距离温度监控难题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 14:10:30

UART接口24位ADC搭配PIC18F46K22的RTD与热电偶测温实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 13:55:19

GD32E10x官方例程入门:从GPIO点灯到串口调试

简介:这份GD32E10x系列官方例程面向嵌入式硬件与单片机开发者,基于ARM Cortex-M3内核,汇集兆易创新GD32E10x的示例代码,适合工业控制、消费电子、通信设备等场景的学习、验证与二次开发。压缩包共935个文件,约11.16MB&…

2026/11/5 13:50:18

基于Django的运维平台实战:资产、发布、巡检、告警一体化

简介:这份资源是一套基于Django框架开发的运维平台源码,面向具备Python基础、希望了解Web运维系统实现思路的开发者与运维人员,可用于学习主机管理、服务监控、日志查看、任务调度、权限控制、资源统计、脚本执行与故障告警等常见运维模块的落…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/5 0:02:07

多智能体协作系统架构设计与实践优化

1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中…

2026/11/5 0:02:07

大模型系统性入门实战指南:从原理到部署微调的全链路路线

提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#…

2026/11/5 0:02:07

鸿蒙文件系统沙箱机制解析与开发实践

1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的…

2026/11/4 12:38:23

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/4 21:04:25

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑