简介:本资源是一份面向高校计算机专业本科生的编译原理课程设计实践材料,完整实现了一个C++编写的词法与语法分析双阶段编译器前端,覆盖DFA驱动的词法分析器和LALR(1)驱动的语法分析器两大核心模块,适用于课程实验、课程设计报告撰写及编译原理原理验证学习。压缩包共17个文件,含3个核心cpp源文件、3个头文件(如SyntaxAnalysis.h、LexicalAnalysis.h)、6个文本配置与过程记录文件(如词法/语法分析输入样例、action/goto表说明)、2份Markdown使用文档、1个PDF+1个DOCX格式的完整课程设计报告,以及1个可直接运行的Compiler.exe可执行文件,整体大小为2.48MB。已有98人学习下载。读者可直接运行程序观察词法识别流程与语法分析栈变化,结合源码理解DFA状态转移机制与LALR(1)分析表构造逻辑,并参考报告中对文法设计、冲突消解及AST构建思路的详细阐述,快速掌握编译前端开发的关键技术路径。
1. 为什么你写的词法分析器总在“识别关键字”时漏掉else,而 LALR(1) 表生成后一跑就 shift/reduce 冲突?——这不是代码 bug,是 DFA 状态爆炸 + LALR(1) 前瞻集计算偏差的双重黑匣子
如果你正卡在编译原理课设的最后两周:手敲完 C++ 版 DFA 词法分析器,测试字符串"if (x > 0) else y = 1;"却把else当成标识符;接着硬啃 LALR(1) 构造流程,手工填完状态转移表,yacc -v一跑却报conflicts: 1 shift/reduce,甚至g++ main.cpp -o parser编译通过但输入合法语句直接段错误——那你不是学得不够,而是掉进了两个经典陷阱:DFA 构造时未处理关键字与标识符的优先级嵌套,以及LALR(1) 的 LR(1) 项集合并策略在小规模文法中反而放大了前瞻符号歧义。这个课设不是考你会不会写switch-case,而是逼你亲手把《编译原理》(龙书)第二章和第四章的抽象数学定义,变成可调试、可断点、可单步验证的 C++ 实体。它适合所有被“理论懂、代码崩”折磨过的本科生——尤其山东科技大学、燕山大学、山科大等校编译原理实验课采用清华第三版教材的群体;也适合想用最小可行代码理解词法/语法分析底层逻辑的 C++ 初学者。本文不讲教科书定义,只拆解:怎么让 DFA 正确区分else和elsewhere,怎么用 C++ 手动构造 LALR(1) 分析表并绕过yacc黑盒,以及为什么你的main()函数里parser.parse("a = b + c;")会崩溃——答案全在状态机跳转条件和goto表索引偏移里。
2. 从正则表达式到可执行 DFA:C++ 实现词法分析器的三步落地法(含完整状态迁移表生成逻辑)
2.1 为什么不能直接手写state == 3 && ch == 'e'?——DFA 必须由 NFA 经子集构造法生成,否则关键字匹配必然出错
很多同学第一步就翻车:为if、else、while等关键字硬编码 if-else 判断,结果elseif被拆成if+else,identifier规则[_a-zA-Z][_a-zA-Z0-9]*与关键字冲突。根本原因在于——词法单元(token)识别本质是正则语言判定问题,必须通过正规式 → NFA → DFA 的标准流程,才能保证最长匹配(longest match)和优先级(keyword > identifier)。清华第三版教材第二章明确要求:关键字必须作为独立终结符参与 NFA 构造,而非后期字符串比对。我们以if、else、int三个关键字 + 标识符 + 数字为例,正规式为:
keyword: (if|else|int) id: [_a-zA-Z][_a-zA-Z0-9]* num: [0-9]+关键点在于:if是id的前缀,若先匹配id,if永远无法被捕获。解决方案是将所有关键字正则式显式加入 NFA 构造,并赋予更高优先级编号。C++ 实现时,我们不调用第三方库,而是手动实现 Thompson 构造法:
// NFAState.h:NFA 状态节点定义 struct NFAState { int id; std::map<char, std::vector<int>> transitions; // char -> [target_state_ids] bool is_accept = false; int token_type = -1; // -1: non-accept, 0: keyword, 1: id, 2: num }; // Thompson 构造核心:or 运算符 (A|B) 对应 ε-闭包合并 std::vector<NFAState> build_nfa_from_regex(const std::string& regex) { // 此处省略具体构造逻辑(需支持连接、或、闭包) // 重点:为每个关键字创建独立接受态,token_type 设为 0 // 为 id 创建接受态,token_type 设为 1 // 最终 NFA 中,多个路径可能到达同一接受态,但 token_type 不同 → 后续 DFA 必须保留此信息 }提示:
token_type字段是后续 DFA 最长匹配的关键。NFA 中一个输入串可能触发多条路径到达不同接受态(如if可达 keyword 接受态,也可达 id 接受态),DFA 构造时需记录所有可能 token_type,最终按编号最小者(即关键字优先级最高)裁决。
2.2 子集构造法:用std::set<int>实现 NFA 状态集合,避免手算状态爆炸
DFA 构造的核心是子集构造法(Subset Construction)。常见错误是试图手算所有状态组合,导致遗漏或重复。C++ 中应直接用std::set<int>表示 NFA 状态集合,用std::map<std::set<int>, int>建立集合到 DFA 状态 ID 的映射:
// DFAState.h struct DFAState { int id; std::map<char, int> transitions; // char -> target_dfa_state_id bool is_accept = false; int final_token_type = -1; // 该状态对应 token 类型(取所有可能 token_type 中编号最小者) }; std::vector<DFAState> nfa_to_dfa(const std::vector<NFAState>& nfa) { std::vector<DFAState> dfa; std::map<std::set<int>, int> set_to_id; std::queue<std::set<int>> q; // 初始状态:ε-closure({0}) std::set<int> start_set = epsilon_closure({0}, nfa); set_to_id[start_set] = 0; dfa.push_back({0, {}, false, -1}); q.push(start_set); while (!q.empty()) { std::set<int> current_set = q.front(); q.pop(); int current_id = set_to_id[current_set]; // 计算每个输入字符的转移 for (char c = 0; c <= 127; ++c) { // ASCII 范围 std::set<int> next_set; for (int nfa_state_id : current_set) { auto it = nfa[nfa_state_id].transitions.find(c); if (it != nfa[nfa_state_id].transitions.end()) { for (int next_id : it->second) { // 并入 ε-closure(next_id) std::set<int> ec = epsilon_closure({next_id}, nfa); next_set.insert(ec.begin(), ec.end()); } } } if (next_set.empty()) continue; // 确定该集合是否已存在 if (set_to_id.find(next_set) == set_to_id.end()) { int new_id = dfa.size(); set_to_id[next_set] = new_id; dfa.push_back({new_id, {}, false, -1}); q.push(next_set); } // 建立转移边 int target_id = set_to_id[next_set]; dfa[current_id].transitions[c] = target_id; // 设置接受态:若 next_set 包含任意 NFA 接受态,取其中最小 token_type int min_type = INT_MAX; for (int nfa_id : next_set) { if (nfa[nfa_id].is_accept && nfa[nfa_id].token_type < min_type) { min_type = nfa[nfa_id].token_type; } } if (min_type != INT_MAX) { dfa[target_id].is_accept = true; dfa[target_id].final_token_type = min_type; } } } return dfa; }这段代码的关键参数说明:
epsilon_closure({0}, nfa):计算初始状态 0 的 ε-闭包,返回所有可通过 ε 边到达的状态集合;c <= 127:仅遍历 ASCII 字符,避免 Unicode 处理复杂度(课设足够);min_type逻辑:确保if(token_type=0)优先于id(token_type=1),解决关键字覆盖问题;dfa[current_id].transitions[c] = target_id:DFA 转移表核心,后续词法分析器直接查此表。
2.3 词法分析器主循环:如何用 DFA 表驱动输入流,正确返回 token 序列?
DFA 构造完成后,词法分析器本质是状态机驱动器。难点在于:如何处理“最长匹配”和“跳过空白”。不能简单读一个字符就返回 token,必须持续推进直到无转移或遇接受态:
// Lexer.h class Lexer { private: std::vector<DFAState> dfa_; std::string input_; size_t pos_ = 0; int current_state_ = 0; public: Lexer(const std::vector<DFAState>& dfa, const std::string& input) : dfa_(dfa), input_(input) {} Token next_token() { int start_pos = pos_; int last_accept_state = -1; int last_accept_pos = -1; int last_accept_type = -1; while (pos_ < input_.length()) { char c = input_[pos_]; // 查 DFA 转移表 auto it = dfa_[current_state_].transitions.find(c); if (it == dfa_[current_state_].transitions.end()) { // 无转移:回退到最近接受态 break; } current_state_ = it->second; pos_++; // 若当前状态为接受态,记录位置和类型 if (dfa_[current_state_].is_accept) { last_accept_state = current_state_; last_accept_pos = pos_; last_accept_type = dfa_[current_state_].final_token_type; } } // 若有接受态,返回对应 token;否则报错 if (last_accept_state != -1) { std::string lexeme = input_.substr(start_pos, last_accept_pos - start_pos); Token tok(last_accept_type, lexeme, start_pos); // 跳过空白(空格、制表、换行) while (pos_ < input_.length() && (input_[pos_] == ' ' || input_[pos_] == '\t' || input_[pos_] == '\n')) { pos_++; } return tok; } else { throw std::runtime_error("Lexical error at position " + std::to_string(start_pos)); } } };逻辑说明:
last_accept_state记录扫描过程中最后一次遇到的接受态,实现最长匹配;lexeme = input_.substr(...)提取实际词素,供后续语法分析使用;- 空白跳过放在 token 返回后,避免影响状态机推进;
throw异常而非return Token(ERROR),强制暴露词法错误位置,方便调试。
3. 从文法到 LALR(1) 分析表:手写 C++ 构造器的四层结构(含 goto 表与 action 表生成细节)
3.1 为什么不用yacc?——课设要求“基于 LALR(1) 分析”,意味着你必须亲手实现 LR(1) 项集族 + 合并 + 表填充
很多同学直接yacc grammar.y生成.tab.c,再用g++编译——这完全违背课设本意。LALR(1) 的核心是:先构造 LR(1) 项集族(每个项形如A → α·β, a),再按核心(core)合并相同左部的项集,最后填充 action 和 goto 表。清华第三版第四章强调:LALR(1) 的优势在于状态数少于 LR(1),但代价是可能引入原本不存在的冲突。C++ 实现必须暴露每一步:
- 文法预处理:添加拓广文法
S' → S,计算 FIRST/FOLLOW; - LR(1) 项集构造:用
std::set<LR1Item>表示每个项集,std::queue<std::set<LR1Item>>BFS 生成; - LALR(1) 合并:对每个项集,提取核心(去掉展望符),将核心相同的项集合并,展望符取并集;
- 表填充:对每个合并后的项集,遍历所有
A → α·Xβ, a,填action[i][a] = shift j或reduce A→α;对非终结符X,填goto[i][X] = j。
我们以经典文法为例(支持赋值、加减):
S' → S S → id = E E → E + T | E - T | T T → id | num3.2 LR(1) 项集族生成:用std::set和std::map实现闭包与转移
LR(1) 项定义为std::tuple<std::string, std::vector<std::string>, int, std::string>:(lhs, rhs, dot_pos, lookahead)。闭包(closure)需递归添加所有X → γ且γ首符号在FIRST(βa)中的项:
struct LR1Item { std::string lhs; std::vector<std::string> rhs; int dot_pos; std::string lookahead; bool operator<(const LR1Item& other) const { if (lhs != other.lhs) return lhs < other.lhs; if (rhs != other.rhs) return rhs < other.rhs; if (dot_pos != other.dot_pos) return dot_pos < other.dot_pos; return lookahead < other.lookahead; } }; std::set<LR1Item> closure(const std::set<LR1Item>& items, const Grammar& g, const std::map<std::string, std::set<std::string>>& first) { std::set<LR1Item> result = items; bool changed = true; while (changed) { changed = false; std::set<LR1Item> new_items; for (const auto& item : result) { if (item.dot_pos >= item.rhs.size()) continue; std::string next_symbol = item.rhs[item.dot_pos]; if (g.is_nonterminal(next_symbol)) { // 计算 βa 的 FIRST:β 是 dot_pos+1 后的符号串,a 是当前 lookahead std::vector<std::string> beta; for (int i = item.dot_pos + 1; i < item.rhs.size(); ++i) { beta.push_back(item.rhs[i]); } std::set<std::string> first_beta_a = first_of_beta_a(beta, item.lookahead, g, first); // 添加所有 A → γ 的项,其中 A = next_symbol for (const auto& prod : g.productions_of(next_symbol)) { for (const std::string& a : first_beta_a) { new_items.insert({prod.lhs, prod.rhs, 0, a}); } } } } for (const auto& ni : new_items) { if (result.find(ni) == result.end()) { result.insert(ni); changed = true; } } } return result; }参数说明:
first_of_beta_a:计算FIRST(βa),若β可推出 ε,则包含a;g.productions_of(next_symbol):获取文法中所有以next_symbol为左部的产生式;std::set<LR1Item>自动去重,依赖operator<实现。
3.3 LALR(1) 合并:用std::map<std::string, std::set<LR1Item>>按核心分组
LALR(1) 合并的本质是:将 LR(1) 项集中所有A → α·β, a归为同一核心,只要A → α·β相同,无论a是什么,都合并。C++ 中,核心可表示为lhs + "|" + join(rhs) + "|" + dot_pos:
std::string core_key(const LR1Item& item) { std::string key = item.lhs + "|"; for (const auto& s : item.rhs) key += s + " "; key += "|" + std::to_string(item.dot_pos); return key; } std::map<std::string, std::set<std::string>> lalr_merge( const std::vector<std::set<LR1Item>>& lr1_itemsets, const Grammar& g) { // step1: 按 core 分组 std::map<std::string, std::set<std::string>> core_to_lookaheads; for (const auto& itemset : lr1_itemsets) { for (const auto& item : itemset) { std::string core = core_key(item); core_to_lookaheads[core].insert(item.lookahead); } } // step2: 重建 LALR 项集 std::vector<std::set<LR1Item>> lalr_itemsets; for (const auto& itemset : lr1_itemsets) { std::set<LR1Item> new_itemset; for (const auto& item : itemset) { std::string core = core_key(item); for (const std::string& la : core_to_lookaheads[core]) { new_itemset.insert({item.lhs, item.rhs, item.dot_pos, la}); } } lalr_itemsets.push_back(new_itemset); } return core_to_lookaheads; // 返回展望符映射,用于后续 action 表填充 }关键点:core_to_lookaheads[core]存储该核心下所有可能的 lookahead 符号,后续填action表时,对每个a∈core_to_lookaheads[core]执行 reduce。
3.4 action/goto 表生成:二维std::vector<std::map<char, Action>>的内存布局与索引技巧
LALR(1) 表由action[i][a](终结符)和goto[i][A](非终结符)组成。C++ 中,action表用std::vector<std::map<std::string, Action>>,goto表用std::vector<std::map<std::string, int>>:
struct Action { enum Type { SHIFT, REDUCE, ACCEPT, ERROR }; Type type; int state_or_prod_id; // SHIFT: target state, REDUCE: production id, ACCEPT: unused }; std::pair<std::vector<std::map<std::string, Action>>, std::vector<std::map<std::string, int>>> build_tables(const std::vector<std::set<LR1Item>>& lalr_itemsets, const Grammar& g, const std::map<std::string, std::set<std::string>>& follow) { std::vector<std::map<std::string, Action>> action(lalr_itemsets.size()); std::vector<std::map<std::string, int>> goto_table(lalr_itemsets.size()); for (int i = 0; i < lalr_itemsets.size(); ++i) { for (const auto& item : lalr_itemsets[i]) { if (item.dot_pos < item.rhs.size()) { std::string next = item.rhs[item.dot_pos]; if (g.is_terminal(next)) { // shift: item → item with dot moved, on symbol 'next' std::set<LR1Item> next_itemset; for (const auto& it : lalr_itemsets[i]) { if (it.lhs == item.lhs && it.rhs == item.rhs && it.dot_pos == item.dot_pos + 1 && it.lookahead == item.lookahead) { next_itemset.insert(it); } } int j = find_itemset_index(lalr_itemsets, next_itemset); if (j != -1) { action[i][next] = {Action::SHIFT, j}; } } else if (g.is_nonterminal(next)) { // goto: nonterminal transition std::set<LR1Item> next_itemset; // ... 类似计算 ... int j = find_itemset_index(lalr_itemsets, next_itemset); if (j != -1) { goto_table[i][next] = j; } } } else { // reduce item: A → α·, a if (item.lhs == "S'" && item.lookahead == "$") { action[i]["$"] = {Action::ACCEPT, 0}; } else { int prod_id = g.production_id(item.lhs, item.rhs); for (const std::string& a : follow.at(item.lhs)) { if (action[i].find(a) != action[i].end()) { // conflict! 课设中需检查并报告 std::cerr << "Conflict at state " << i << ", symbol " << a << std::endl; } action[i][a] = {Action::REDUCE, prod_id}; } } } } } return {action, goto_table}; }参数说明:
find_itemset_index:在lalr_itemsets中查找next_itemset对应的索引,需实现集合相等比较;follow.at(item.lhs):获取左部符号的 FOLLOW 集,用于 reduce 的展望符;action[i][a]冲突检测:若同一(i,a)既有 shift 又有 reduce,即 shift/reduce 冲突,需在课设报告中分析原因(如文法二义性)。
4. 避坑:词法与语法分析器集成时的 5 个血泪经验(现象→原因→解决)
4.1 现象:词法分析器返回Token(ID, "if"),但语法分析器报syntax error before 'if'
原因:词法分析器输出的Token结构体未定义operator==或std::hash,导致std::map<std::string, int>查action表时键比较失败;或Token.type与action表中终结符字符串不一致(如词法返回"ID",但表中用"id")。
解决:统一终结符命名规范。在Token类中重载operator==和operator<,或直接用整型枚举enum TokenType { ID, IF, ELSE, ASSIGN, PLUS, ... },action表索引用TokenType而非字符串。修改Lexer::next_token()返回Token{ID, "if"},action[i][ID]查表。
4.2 现象:LALR(1) 表生成无冲突,但parser.parse("a = b + c;")运行时栈溢出或段错误
原因:std::stack<Token>在shift时 push 了Token对象,但Token包含std::string lexeme,其内部缓冲区在多次 push 后因内存重分配失效;或goto表索引越界(j超出lalr_itemsets.size())。
解决:Token类禁用拷贝,改用移动语义;std::stack改为std::vector<Token>+size_t top_index手动管理;goto_table[i][nonterm]查找前加边界检查if (j < lalr_itemsets.size())。
4.3 现象:输入"if (a > 0) x = 1;"词法分析正确,但语法分析停在(报错
原因:文法未定义括号规则,或FIRST集计算错误导致E → T无法匹配(;更常见的是:词法分析器将(识别为LPAREN,但action表中state_i["("]为空(未生成该转移)。
解决:打印所有lalr_itemsets[i],确认是否存在E → T ·, (项;用std::cout << "state " << i << ": "; for (auto& it: itemset) cout << it.lhs << "->" << ...调试;确保FIRST计算包含((即T的FIRST包含()。
4.4 现象:g++ -o parser main.cpp lexer.cpp parser.cpp成功,但./parser运行闪退,gdb显示SIGSEGV在action[current_state].at(lookahead)
原因:std::map::at()抛出std::out_of_range异常,但未捕获;或lookahead字符串为空(如词法分析器返回Token(EOF, ""),但action表未定义""键)。
解决:action[current_state]改用find()而非at(),检查it != action[current_state].end();Token构造时,EOF的lexeme设为"$",action表必须包含["$"]。
4.5 现象:课程设计报告要求“含可执行文件”,但./parser在同学电脑上运行报libstdc++.so.6: version 'GLIBCXX_3.4.29' not found
原因:编译环境 GCC 版本过高(如 13.x),生成的二进制依赖新版 libstdc++,而目标机器(如实验室 Ubuntu 20.04)只有 GLIBCXX_3.4.28。
解决:编译时加-static-libstdc++链接静态 stdc++ 库;或指定低版本 GCC 编译:g++-11 -std=c++17 -o parser ...;课设交付时附ldd parser输出,证明无外部依赖。
5. 从源码到可执行:VSCode + CMake 调试全流程(含 Windows/Linux 双平台配置要点)
5.1 VSCode 配置c_cpp_properties.json:为什么#include "DFAState.h"总标红,但g++编译成功?
这是 VSCode C/C++ 插件的 IntelliSense 路径与实际编译器路径不一致导致的。关键不是includePath,而是browse.path和compilerPath必须指向真实工具链:
// .vscode/c_cpp_properties.json { "configurations": [ { "name": "Linux", "includePath": ["${workspaceFolder}/**", "/usr/include/c++/11"], "defines": [], "compilerPath": "/usr/bin/g++-11", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-x64", "browse": { "path": ["${workspaceFolder}", "/usr/include/c++/11"] } }, { "name": "Win32", "includePath": ["${workspaceFolder}/**", "C:/msys64/mingw64/include/c++/11"], "compilerPath": "C:/msys64/mingw64/bin/g++.exe", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "gcc-x64", "browse": { "path": ["${workspaceFolder}", "C:/msys64/mingw64/include/c++/11"] } } ], "version": 4 }注意:
/usr/include/c++/11路径需根据实际 GCC 版本调整(ls /usr/include/c++/查看);Windows 下用 MSYS2 MinGW-w64,避免 Microsoft Visual C++ Redistributable 版本混乱。
5.2 CMakeLists.txt:如何让add_executable(parser main.cpp)自动链接所有分析器模块?
课设源码通常分散为lexer/,parser/,dfa/目录。CMake 必须显式添加子目录并导出接口:
# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(CompilerDesign LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加子目录 add_subdirectory(lexer) add_subdirectory(parser) add_subdirectory(dfa) # 主可执行文件 add_executable(parser main.cpp) target_link_libraries(parser PRIVATE lexer parser dfa) target_include_directories(parser PRIVATE ${CMAKE_SOURCE_DIR}) # 导出头文件路径 install(TARGETS parser DESTINATION bin)# lexer/CMakeLists.txt add_library(lexer STATIC Lexer.cpp DFAState.cpp) target_include_directories(lexer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})关键点:target_link_libraries(parser PRIVATE lexer ...)中PRIVATE表示链接关系不传递,避免循环依赖;target_include_directories用PUBLIC使lexer的头文件对parser可见。
5.3 调试技巧:在Lexer::next_token()中设置条件断点,只停在lexeme=="if"时
VSCode 调试器支持 GDB/LLDB 条件断点。在Lexer.cpp第 45 行(Token tok(...))右键 → “Add Conditional Breakpoint”,输入:
lexeme == "if"这样无需手动单步,GDB 会自动在每次识别到if时暂停。配合print current_state_和print dfa_[current_state_].transitions查看当前 DFA 状态转移,验证if是否进入正确接受态。
5.4 Windows 下生成真正可移植的.exe:为什么dev-c++编译的程序在另一台 Win10 上报错0xc000007b?
0xc000007b是典型的架构不匹配(32/64 位)或 DLL 缺失。dev-c++默认 MinGW 32 位,而现代 Win10 多为 64 位。解决方案:
- 彻底弃用 dev-c++,改用 MSYS2 MinGW-w64(64 位);
- 编译时加
-static参数:g++ -static -o parser.exe main.cpp ...,静态链接所有依赖; - 交付前用
ntldd -R parser.exe(MSYS2 中)检查依赖 DLL,确保输出只有KERNEL32.dll、msvcrt.dll等系统 DLL。
5.5 Linux 下一键打包可执行文件:cpack生成.tar.gz并验证无动态依赖
课设交付要求“含可执行文件”,但直接交parser二进制可能因 GLIBC 版本失败。用 CPack 打包源码+编译脚本:
# CMakeLists.txt 末尾添加 include(CPack) set(CPACK_GENERATOR "TGZ") set(CPACK_PACKAGE_NAME "CompilerDesign") set(CPACK_PACKAGE_VERSION "1.0.0") set(CPACK_SOURCE_IGNORE_FILES "/build/;/CMakeFiles/;.git;")然后:
mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make cpack -G TGZ生成CompilerDesign-1.0.0-Linux.tar.gz,解压后含build.sh:
#!/bin/bash g++-11 -std=c++17 -O2 -static-libstdc++ -o parser *.cpp echo "Built parser successfully"交付时附build.sh,确保同学在自己环境编译,规避 ABI 问题。
6. 课设报告与答辩的隐藏得分点:如何用三张图讲清 DFA/LALR(1) 的不可替代性
6.1 图一:DFA 状态图对比——手写 vs 子集构造法,为什么后者能消灭else误识别?
不要贴大段代码,用 Graphviz 画两个状态图。左侧“手写 DFA”:start -> e -> l -> s -> e为else,但e -> i -> f为if,e -> l -> s -> e -> w -> h -> e -> r -> e为elsewhere,问题在于else状态未标记为接受态,或接受态优先级低于id。右侧“子集构造 DFA”:标注state_5: accept, token_type=0 (keyword),state_12: accept, token_type=1 (id),箭头旁注e: goto 1, l: goto 2, ...,并加文字框:“else的接受态 token_type=0 <id的 token_type=1,最长匹配时自动选择”。
这张图的价值:直观证明课设要求的‘基于 DFA’不是形式主义,而是解决关键字歧义的数学保障。答辩时指着图说:“如果 hand-write,elsewhere的前缀else会被截断;子集构造确保所有前缀路径收敛到同一接受态,靠 token_type 编号裁决。”
6.2 图二:LALR(1) 项集合并示意图——为什么合并后状态数减少,却引入了 shift/reduce 冲突?
画两个 LR(1) 项集:`
本文还有配套的精品资源,点击获取