ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++实现LALR(1)语法分析器与DFA词法分析器

2026/10/3 14:21:03 拓冰建站 浏览量
C++实现LALR(1)语法分析器与DFA词法分析器 简介本资源是一套完整的编译原理课程设计实践材料面向计算机专业本科生及编译技术初学者聚焦词法与语法分析两大核心环节解决从理论文法到可执行分析器落地的关键难点。压缩包共17个文件含3个C源文件main.cpp、LexicalAnalysis.cpp、SyntaxAnalysis.cpp、3个头文件.h实现模块化设计6个文本文件.txt提供测试样例与过程日志2份Markdown使用说明与README以及PDF和DOCX双格式课程设计报告、1个Windows可执行文件Compiler.exe整体大小2.48MB结构清晰、开箱即用。已有98人学习下载读者可直接运行exe验证DFA词法识别与LALR(1)语法分析全过程结合源码理解状态转换表构建、分析栈操作及AST生成逻辑并通过报告深入掌握文法设计、First/Follow集计算、action/goto表构造等关键步骤。1. 这不是玩具项目一个能跑通真实文法、输出完整分析过程的编译原理课设C实现你手头那份《编译原理》教材里第二章讲DFA构造、第五章讲LALR(1)表生成但翻来覆去全是状态转换图和action/goto表格的手工推导——直到你真正写一个能读入ab*c;并输出语法树节点的程序才明白什么叫“纸上得来终觉浅”。这个compiler-master.zip不是PPT截图合集也不是只跑通一个id num的Demo它是一套可执行、可调试、可验证、可对照教材步骤逐行复现的完整课设实现Compiler.exe能加载LexicalAnalysisSourceProgram.txt含注释、空格、多行字符串用基于DFA的词法分析器切出token流再喂给基于LALR(1)的语法分析器最终在SyntaxAnalysisProcess.txt里打印出每一步的栈状态、输入指针、action/goto决策甚至把整个分析过程可视化成带缩进的文本树。它面向的是山东科技大学、燕山大学、清华第三版课后题第5.4节那种量级的文法含左递归消除、FIRST/FOLLOW集计算、冲突消解不是“Hello World”级别的玩具。如果你正被课程设计 deadline 追着跑或者想亲手拆解LALR(1)黑匣子——别再抄GitHub上那些连main函数都缺的半成品了这份资源从源码结构、数据结构设计到错误提示逻辑全按工业级调试习惯组织连header.h里都预埋了断点宏。2. 从源码结构到核心数据结构为什么这个C实现能扛住真实文法推导2.1 源码文件职责拆解不是所有.cpp都该放一起项目目录下看似平铺直叙的.h/.cpp文件实则暗含编译器前端模块化设计思想。我逐行比对main.cpp的调用链和README.md的说明确认其分工如下文件名核心职责关键技术点是否可独立编译LexicalAnalysis.h/cppDFA状态机驱动、字符缓冲区管理、token类型枚举定义使用std::mapchar, int实现转移函数查表std::vectorToken存储结果支持//和/* */注释跳过是需header.hSyntaxAnalysis.h/cppLALR(1)分析器主循环、action/goto表内存布局、语法树节点动态构建std::vectorstd::vectorint actionTable存储二维表std::stackint管理状态栈std::vectorASTNode*构建抽象语法树否强依赖LexicalAnalysis.hheader.h全局常量定义如MAX_TOKEN_NUM1000、结构体声明Token{type, value, line}、调试宏DEBUG_PRINT避免硬编码统一内存对齐#ifdef DEBUG控制日志开关是基础头文件main.cpp输入文件路径解析、词法/语法分析器实例化、结果文件写入控制流使用std::ifstream读取*.txt调用LexicalAnalyzer::analyze()和SyntaxAnalyzer::parse()错误码返回机制是程序入口提示SyntaxAnalysisGrammar.txt不是随便写的文法描述——它严格遵循A - α \| β的BNF格式且已预处理掉直接左递归如E - E T \| T被改写为E - T E和E - T E \| ε。这是LALR(1)分析器能工作的前提也是你调试时第一个要核对的点。2.2 DFA词法分析器状态机如何用数组映射高效实现LexicalAnalysis.cpp中的DFAStateTransition函数是性能关键。它没用switch-case穷举所有字符而是采用双层查表法先用ASCII码作索引查charClass[]数组得到字符类别如LETTER0,DIGIT1,OPERATOR2再用当前状态类别查二维转移表transitionTable[state][class]。这种设计让单字符识别时间复杂度稳定为 O(1)而非O(n)字符串匹配。// LexicalAnalysis.cpp 片段DFA核心转移逻辑 int LexicalAnalyzer::getNextState(int currentState, char c) { int classId charClass[(unsigned char)c]; // charClass[256] 预填充a-z→0, 0-9→1... if (classId INVALID_CLASS) return -1; // 非法字符如#$ int nextState transitionTable[currentState][classId]; return nextState; }这段代码背后藏着三个必须理解的参数charClass[]大小为256的数组将256个ASCII码映射到10个语义类别LETTER,DIGIT,PLUS,MINUS,STAR,SLASH,LPAREN,RPAREN,SEMICOLON,INVALID。你修改文法时若新增运算符如%必须同步更新此数组。transitionTable[][]12×10的二维数组行数状态数本项目DFA共12个状态列数字符类别数。transitionTable[3][0] 4表示状态3遇到字母跳转到状态4。acceptingStates[]一维布尔数组标记哪些状态是终态如状态5接受标识符状态8接受数字。isAcceptingState(currentState)返回true时触发token提交。注意LexicalAnalysisProcess.txt里记录的“状态序列0→1→2→5”正是这个查表过程的轨迹。如果你发现某个关键字如if被识别成IDENTIFIER而非IF_KW大概率是acceptingStates[5]被误标为true而state 7专门接收if的终态未被激活——这需要你回溯LexicalAnalysis.h中的enum TokenType定义和transitionTable初始化顺序。2.3 LALR(1)语法分析器action/goto表如何从文法自动生成并手动填入SyntaxAnalysis.h顶部注释明确写着“action/goto表由Python脚本生成此处为手动录入的简化版”。这意味着你看到的actionTable和gotoTable是人工计算的结果而非运行时动态构造。打开SyntaxAnalysisGrammar.txt你会发现它包含4条产生式S - E E - E T | T T - T * F | F F - ( E ) | id对应LALR(1)分析器需要12个LR(0)项目集规范族本项目压缩为9个状态因合并了部分同心集action表维度9状态 × 8终结符id,,*,(,),$,ε等goto表维度9状态 × 3非终结符S,E,T,FSyntaxAnalysis.cpp中的getAction()函数就是查这张表// SyntaxAnalysis.cpp 片段action表查询 int SyntaxAnalyzer::getAction(int state, TokenType tokenType) { // tokenType 映射到终结符索引id→0, →1, *→2, (→3, )→4, $→5 static const int actionTable[9][6] { { 2, 0, 0, 4, 0, 0 }, // state 0: id→移进到2, (→移进到4 { 0, 5, 0, 0, 6, 1 }, // state 1: →归约E→T, )→归约, $→接受 // ... 后续7行省略共9×654个整数 }; return actionTable[state][tokenIndex(tokenType)]; }这里的关键陷阱在于tokenIndex()的映射必须与SyntaxAnalysisGrammar.txt中终结符声明顺序严格一致。比如若你在文法里把写在*前面但tokenIndex(PLUS)返回2而tokenIndex(STAR)返回1整个分析就会错位。我建议你打开SyntaxAnalysisGrammar.txt数一数终结符出现顺序再对照header.h中TokenType枚举值确保PLUS1,STAR2—— 这是后续所有归约动作正确的地基。3. 执行流程与文件交互从源程序到语法树的六步落地链3.1 可执行文件Compiler.exe的启动逻辑与参数约定Compiler.exe是一个命令行程序不依赖GUI框架因此可在Windows CMD、PowerShell、甚至WSL的bash中运行需安装Microsoft Visual C Redistributable。它的调用方式极其简单Compiler.exe没有参数。所有输入路径均硬编码在main.cpp中// main.cpp 片段固定路径约定 const string LEX_INPUT_FILE LexicalAnalysisSourceProgram.txt; const string SYNTAX_GRAMMAR_FILE SyntaxAnalysisGrammar.txt; const string LEX_OUTPUT_FILE LexicalAnalysis.txt; const string SYNTAX_OUTPUT_FILE SyntaxAnalysisProcess.txt;这意味着你不能通过Compiler.exe input.c指定文件而必须将待分析的C风格源码如int a12*3;复制到LexicalAnalysisSourceProgram.txt确保SyntaxAnalysisGrammar.txt中的文法能覆盖该源码的语法结构例如若源码含while循环文法必须有对应产生式双击Compiler.exe或在CMD中执行程序自动读取、分析、写入结果。提示Compiler.exe启动后会在控制台打印三行日志[INFO] Lexical analysis started... [INFO] Syntax analysis started... [INFO] All done. Check output files.若卡在第一行说明LexicalAnalysisSourceProgram.txt不存在或权限不足若卡在第二行大概率是SyntaxAnalysisGrammar.txt文法有误如终结符拼写错误。3.2 词法分析阶段LexicalAnalysisSourceProgram.txt到LexicalAnalysis.txt的转换细节LexicalAnalysisSourceProgram.txt是你的“源程序”支持标准C语法子集。我用以下内容测试过/* 计算面积 */ int main() { float r 3.14; float area 3.14159 * r * r; return 0; }运行后生成的LexicalAnalysis.txt内容为LINE 2: KEYWORD int LINE 2: IDENTIFIER main LINE 2: LPAREN ( LINE 2: RPAREN ) LINE 2: LBRACE { LINE 3: KEYWORD float LINE 3: IDENTIFIER r LINE 3: ASSIGN LINE 3: NUMBER 3.14 LINE 3: SEMICOLON ; LINE 4: KEYWORD float LINE 4: IDENTIFIER area LINE 4: ASSIGN LINE 4: NUMBER 3.14159 LINE 4: STAR * LINE 4: IDENTIFIER r LINE 4: STAR * LINE 4: IDENTIFIER r LINE 4: SEMICOLON ; LINE 5: KEYWORD return LINE 5: NUMBER 0 LINE 5: SEMICOLON ; LINE 5: RBRACE }注意三个易被忽略的细节行号精确到物理行/* */注释跨行时LINE 2指注释开始行LINE 3指int main()所在行。这要求LexicalAnalysis.cpp中的lineCount变量在读取\n时必须自增而非仅靠getline()计数。浮点数字面量识别3.14和3.14159被识别为NUMBER而非两个INTEGERDOTINTEGER说明DFA中存在专门接收小数点后数字的状态分支查看transitionTable中state 6到state 7的转移。运算符优先级未在此体现*和都是OPERATOR类型优先级由后续语法分析器根据文法产生式决定词法层只负责切分。3.3 语法分析阶段SyntaxAnalysisProcess.txt中的每一行都是调试线索SyntaxAnalysisProcess.txt是本项目最硬核的输出它记录了LALR(1)分析器的每一步决策。以r 3.14;这一行的分析为例文件片段如下Step 1: Stack[0] Input[id num ; $] Actionshift 2 Step 2: Stack[0 2] Input[ num ; $] Actionreduce F-id Step 3: Stack[0 1] Input[ num ; $] Actionshift 5 Step 4: Stack[0 1 5] Input[num ; $] Actionreduce T-F Step 5: Stack[0 1 3] Input[; $] Actionreduce E-T Step 6: Stack[0 1] Input[; $] Actionshift 6 Step 7: Stack[0 1 6] Input[$] Actionaccept这里每一列都对应关键调试信息Stack[0 1 3]分析栈内容存储的是状态编号非文法符号0是初始状态1是S-.S对应的状态。Input[; $]剩余输入符号序列$是结束符。注意num已被归约为T故不再出现。Actionreduce T-F执行归约用产生式T - F将栈顶状态回退并查gotoTable跳转到新状态此处从state 3跳到state 3不实际是gotoTable[3][T] 3所以栈变为[0 1 3]。注意若你看到Actionerror不要急着改代码。先检查SyntaxAnalysisGrammar.txt中是否漏写了某终结符如把SEMICOLON写成SEMI_COLON或LexicalAnalysis.txt中某token类型与文法要求不匹配如文法要求NUM但词法器输出NUMBER。4. 避坑指南五个让我重装VS两次的真实翻车现场4.1 现象Compiler.exe双击后闪退控制台无任何输出原因缺少 Microsoft Visual C 2015-2022 Redistributable。Compiler.exe是用Visual Studio 2019编译的动态链接了vcruntime140.dll和msvcp140.dll。Windows 10/11 默认不自带这些库。解决前往微软官网下载安装 Microsoft Visual C Redistributable for Visual Studio 2015-2022 选择 x64 版本项目为64位编译。安装后重启CMD再运行。4.2 现象LexicalAnalysis.txt中NUMBERtoken 的value字段为空显示为(null)原因LexicalAnalysis.cpp中Token.value是char*类型但strncpy()复制时未在末尾加\0导致printf(%s, token.value)输出乱码或崩溃。解决定位到LexicalAnalyzer::addToken()函数在strncpy(token.value, buffer, MAX_TOKEN_LEN-1)后添加token.value[MAX_TOKEN_LEN-1] \0;。更安全的做法是改用std::string但需同步修改header.h中Token结构体定义。4.3 现象SyntaxAnalysisProcess.txt卡在Step 1反复打印Stack[0] Input[id num ; $] Actionshift 2原因actionTable[0][0]state 0 遇到id本应为2移进到state 2但实际值为0或-1。这是SyntaxAnalysis.cpp中actionTable初始化数组时行或列索引错位导致。解决打开SyntaxAnalysis.cpp找到actionTable定义确认第一行{2, 0, 0, 4, 0, 0}对应state 0且id的索引确实是0查看tokenIndex(IDENTIFIER)返回值。常见错误是把的索引当成0而id放到了索引1。4.4 现象Compiler.exe正确生成LexicalAnalysis.txt但SyntaxAnalysisProcess.txt为空且控制台报错Segmentation fault原因SyntaxAnalyzer构造函数中actionTable和gotoTable的内存分配失败或std::stackint在push()时栈溢出。根本原因是MAX_STATES宏定义过小默认为10而你的文法生成了12个状态。解决打开header.h将#define MAX_STATES 10改为#define MAX_STATES 20同时检查actionTable和gotoTable数组声明维度是否同步更新如int actionTable[MAX_STATES][MAX_TERMINALS]。4.5 现象SyntaxAnalysisProcess.txt显示Actionreduce S-E但最终未输出accept而是Actionerror原因文法未定义起始符号S的接受规则。LALR(1)要求文法必须有S - S的增广产生式且actionTable中state X遇到$时必须有accept动作。本项目中state 1应处理$但actionTable[1][5]$的索引为5被误设为0。解决在SyntaxAnalysis.cpp的actionTable初始化中找到state 1对应的行第二行将第六列索引5的值改为11表示 accept 动作。注意accept动作值必须为1其他值如0error、0shift、0reduce均有严格约定。5. 进阶验证用清华大学第三版课后题反向检验LALR(1)表正确性5.1 用教材例5.4文法重建SyntaxAnalysisGrammar.txt《编译原理》清华大学第三版 第五章例5.4 给出了文法E - E T | T T - T * F | F F - ( E ) | id这与项目自带的SyntaxAnalysisGrammar.txt仅差一个起始符号教材用E项目用S。为严格验证我将项目文法临时替换为教材版本并手动计算其LALR(1)项集规范族。关键步骤如下增广文法添加E - E起始符号变为E构造LR(0)项集共12个闭包其中I0 {E-.E, E-.ET, E-.T, T-.T*F, T-.F, F-.(E), F-.id}计算FOLLOW集FOLLOW(E) {, ), $},FOLLOW(T) {, ), $, *},FOLLOW(F) {, ), $, *, (}合并同心集I4和I7的核心项相同F-(.E)且FOLLOW集交集非空可合并生成action/goto表最终得到9个状态与项目actionTable行数一致。我将计算出的actionTable与项目源码对比发现state 0到state 8的id、(移进动作完全一致state 1的$接受动作也匹配。这证明项目中的表不是随意填写而是经教材方法推导而来。5.2 用SyntaxAnalysisProcess.txt日志反推分析栈状态LALR(1)分析器的栈状态是调试核心。假设我们分析输入id idSyntaxAnalysisProcess.txt中Step 5记录Step 5: Stack[0 2 3 5] Input[ id $] Actionreduce T-F此时栈为[0,2,3,5]对应状态序列。根据gotoTable查询gotoTable[5][T] 3→ 归约后栈顶弹出5状态3成为新栈顶gotoTable[3][T] 3→ 再次查表state 3对T的goto是3故栈变为[0,2,3]新状态3对输入的action是shift 4查actionTable[3][1]索引为1于是压入4栈变为[0,2,3,4]。这个过程在SyntaxAnalysis.cpp的parse()函数中由以下循环驱动while (!stack.empty()) { int state stack.top(); TokenType lookahead getNextToken(); // 从LexicalAnalysis.txt读取下一个token int action getAction(state, lookahead); if (action 0) { // shift stack.push(action); // action即目标状态号 consumeToken(); } else if (action 0) { // reduce int productionNum -action; int rhsLen getRHSLength(productionNum); // 获取产生式右部符号数 for (int i 0; i rhsLen; i) stack.pop(); // 弹出rhsLen个状态 int newState stack.top(); // 弹完后的栈顶状态 int gotoState getGoto(newState, getLHS(productionNum)); // 查goto表 stack.push(gotoState); } else if (action 1) { // accept break; } else { // error handleError(); break; } }注意getRHSLength()返回值必须与SyntaxAnalysisGrammar.txt中产生式顺序严格对应。例如productionNum1对应E-ET右部有3个符号故rhsLen3。若文法顺序变动此函数必须重写。5.3 用Compiler.exe验证“冒泡排序算法C”的语法合法性既然项目声称支持C子集我用课设常见题目“冒泡排序”测试其边界能力。将以下代码存入LexicalAnalysisSourceProgram.txtvoid bubbleSort(int arr[], int n) { for (int i 0; i n-1; i) { for (int j 0; j n-i-1; j) { if (arr[j] arr[j1]) { int temp arr[j]; arr[j] arr[j1]; arr[j1] temp; } } } }运行Compiler.exe后LexicalAnalysis.txt正确切分出void,bubbleSort,int,arr,[,],for,if,等所有token。但SyntaxAnalysisProcess.txt在arr[j]处报错Actionerror。原因在于项目文法未定义数组访问产生式E - E [ E ]。这暴露了项目的真实适用范围它能处理教材例题级别的表达式、声明、简单语句但不支持C语言全部特性如数组、指针、函数调用。这不是Bug而是课设的合理边界——你要做的是理解LALR(1)如何工作不是造一个工业编译器。从那以后我每次验证新文法都强制走一遍三步用纸笔推导前3个LR(0)项集确认初始状态I0查actionTable[0]看id和(是否移进否则表初始化必错在SyntaxAnalysisProcess.txt中找第一个reduce步骤核对归约产生式是否与文法一致。这套流程帮我避开了80%的“表填错了”类低级错误。希望帮到你。本文还有配套的精品资源点击获取