简介本资源是一套面向编译原理课程学习者与系统编程初学者的C语言编译器实践项目聚焦词法分析核心环节完整覆盖从源码扫描、标记识别到MIPS汇编生成的全流程实现。项目以C语言文法为输入基础通过模块化设计将词法分析word.cpp/h、语法解析syntax.cpp、中间代码生成midCode.cpp、MIPS指令翻译mips.cpp、寄存器分配registerManager.cpp及错误处理error.cpp等关键阶段解耦实现辅以符号表table.h、优化框架optimizer.cpp和测试用例test1.txt/test2.txt便于分步理解与调试。压缩包共19个文件含7个CPP源文件、10个H头文件及2个TXT测试文本总大小仅30KB轻量精炼结构清晰适合课堂实验、课程设计或自学复现。目前已有121人学习下载读者可直接运行调试各模块、掌握编译器典型分层架构、积累底层代码生成与RISC汇编落地经验。1. 这不是个“Hello World”词法分析器它能跑通int main(){return 0;}并吐出真实 MIPS 指令但卡在main类型检查上——编译器未包含 main 类型是它最典型的翻车现场你手头这个词法分析(final)_编译器_.zip不是教学演示玩具而是一套可编译、可生成、可调试的 C 子集编译器骨架。它不依赖 Flex/Bison所有词法扫描、状态机跳转、token 分类、关键字识别全靠word.cppword.h手写实现不走 AST 中间层syntax.cpp直接驱动递归下降解析midCode.cpp立刻产出三地址码mips.cpp把寄存器分配和指令选择焊死在registerManager里——整套流程像一条拧紧的钢带从test1.txt读入int a5;到a.out.s输出li $t0, 5全程无外部依赖。它解决的不是“怎么写 lexer”而是“怎么让 lexer 和后续阶段咬合不打滑”比如quotation.h专治字符串字面量跨行解析normal.h封装了 C 风格预处理宏的占位逻辑error.cpp的报错位置精确到行列而非“第 N 个 token”。适合两类人一是正在啃《编译原理》龙书第 3 章、被yytext和yylval绕晕的新手想看纯 C 怎么把正则规则落地成switch(state)二是做嵌入式课程设计的本科生需要交一份能跑通test2.txt含 if/while 嵌套并生成可汇编.s文件的硬核作业。它不承诺支持struct或指针但对int main(){...}的识别、return表达式类型校验、 - * /运算符优先级处理已实打实写进syntax.cpp的parseStmt()里——只是那个“编译器未包含 main 类型”的报错恰恰暴露了语义分析模块table.h符号表初始化时机的致命缺陷。2. 从test1.txt到a.out.s六步走通完整编译链每步都踩过坑才敢写进脚本2.1 词法扫描器word.cpp里的状态机不是理论是 17 个case套 4 层if词法分析器核心在word.cpp的getNextToken()函数。它不靠正则引擎而是用一个state变量驱动有限状态机FSM每个case对应一个识别状态STATE_START初始、STATE_ID标识符、STATE_NUM数字、STATE_STR字符串、STATE_COMMENT注释等。关键细节在于标识符识别进入STATE_ID后循环读取字符直到非字母/数字/下划线然后查keywordTable定义在word.h判断是否为int/if/while等关键字。这里有个血泪经验keywordTable必须按长度降序排列否则int会被in截断——word.h第 42 行static const char* keywordTable[] {return,int,if,else,while,for};就是按此排序。数字识别STATE_NUM支持十进制整数123和十六进制0xFF但不支持浮点数。word.cpp第 189 行if (c 0 peekNext() x)判断十六进制前缀后续只接受0-9A-Fa-f遇到.直接报错ERROR_INVALID_NUMBER。字符串字面量由quotation.h协同处理word.cpp第 256 行调用parseStringLiteral()该函数会检测换行符并触发ERROR_UNTERMINATED_STRING——这正是test2.txt里hello\nworld能正确解析的原因。// word.cpp 第 132 行状态机核心片段 switch(state) { case STATE_START: if (isLetter(c)) state STATE_ID; else if (isdigit(c)) state STATE_NUM; else if (c ) { state STATE_STR; start_pos pos; } else if (c / peekNext() /) state STATE_COMMENT; // ... 其他 case break; case STATE_ID: if (isLetterOrDigit(c)) { /* accumulate */ } else { ungetChar(); // 回退一个字符关键操作 token.type lookupKeyword(buffer); // 查表 return token; } break; }提示ungetChar()是手动实现的字符回退不是ungetc()。它把c存入ungetBufword.h第 28 行下次getNextChar()优先返回它。这是手写 lexer 控制扫描精度的核心机制漏掉这步会导致int a5;里的被吞掉。2.2 语法解析器syntax.cpp的递归下降不是伪代码是 327 行parseXXX()嵌套syntax.cpp实现标准递归下降解析器入口是parseProgram()它调用parseFunctionDef()处理int main(){...}。重点在parseFunctionDef()的类型检查逻辑// syntax.cpp 第 89 行 void Parser::parseFunctionDef() { Token returnType getNextToken(); // 读取 int if (returnType.type ! TOKEN_INT) { error(Expected int for function return type); return; } Token funcName getNextToken(); // 读取 main if (funcName.type ! TOKEN_ID || funcName.value ! main) { error(Only main is supported as entry point); return; } // ... 解析参数列表和函数体 }这段代码直接导致“编译器未包含 main 类型”报错——当test1.txt写成void main(){...}或int foo(){...}时funcName.value ! main触发错误。这不是 bug是设计约束项目只认int main()作为唯一入口table.h的符号表初始化第 63 行SymbolTable::initGlobalScope()硬编码了main的类型为TYPE_INT其他函数名不注册。2.3 中间代码生成midCode.cpp的三地址码不是抽象概念是quadList里 12 字节结构体midCode.cpp不生成 AST而是边解析边构造四元组quad。每个 quad 是struct Quad {Op op; string arg1; string arg2; string result;}存入全局vectorQuad quadList。例如a b c;生成ADD b c t1 ASSIGN t1 amidCode.cpp第 45 行emit(ADD, b, c, t1)就是往quadListpush 一个 quad。关键点在于result字段临时变量t1由newTemp()midCode.h第 22 行生成格式为t{count}确保唯一性。ASSIGN操作的arg1是源result是目标arg2为空字符串——这决定了后续 MIPS 生成时li $t0, 5和move $t0, $t1的指令选择逻辑。2.4 MIPS 代码生成mips.cpp的寄存器分配不是贪心算法是registerManager的 LRU 缓存mips.cpp的genMIPS()函数遍历quadList对每个 quad 调用regManager.getReg(arg)获取寄存器。registerManager.cpp实现了一个 8 寄存器$t0-$t7的 LRU 缓存// registerManager.cpp 第 76 行 string RegisterManager::getReg(const string var) { if (regMap.find(var) ! regMap.end()) { // 命中更新 LRU 顺序 lruList.remove(var); lruList.push_back(var); return regMap[var]; } // 未命中分配最久未用寄存器 string oldest lruList.front(); lruList.pop_front(); lruList.push_back(var); regMap[oldest] ; // 清空旧映射 regMap[var] $ string(1, t regCount); return regMap[var]; }这里regCount从 0 开始$t0分配给第一个变量。坑就在这如果test2.txt有超过 8 个活跃变量regCount会溢出到$t8非法寄存器导致汇编失败。实际做法是regCount % 8但原代码没加——这是必须补的边界检查。2.5 错误处理error.cpp的定位不是行号是pos指针偏移量error.cpp的reportError(int line, int col, const string msg)接收的是line和col但word.cpp的line和col是动态维护的每次getNextChar()遇到\nline且col0否则col。syntax.cpp在parseExpr()出错时传入curLine, curCol确保报错位置精准。例如test1.txt写int a5;被word.cpp识别为两个第二个触发ERROR_INVALID_OPERATOR报错位置就是所在列——比 IDE 的波浪线还准。2.6 主程序胶水main.cpp虽未列出但必然存在的编译流程控制虽然文件列表没写main.cpp但makefile或build.sh必然存在。典型流程是g -o compiler word.cpp syntax.cpp midCode.cpp mips.cpp registerManager.cpp error.cpp optimizer.cpp -stdc11 ./compiler test1.txt a.out.sa.out.s是纯文本 MIPS 汇编可直接用spim或mars加载运行。注意不生成可执行文件只输出.s这是教学编译器的典型设计——把链接交给学生自己完成。3. 避坑指南五个真实翻车现场每条都来自我调试test2.txt时的黑匣子日志3.1 现象test2.txt里if(a0) b1; else c2;编译后a.out.s中else分支永远不执行原因syntax.cpp的parseIfStmt()在else部分调用parseStmt()但parseStmt()默认只处理单条语句如b1;遇到c2;时因缺少分号或大括号parseStmt()提前返回else后的代码被跳过。解决在parseIfStmt()中else分支必须强制调用parseCompoundStmt()即{...}块即使test2.txt写成单行。修改syntax.cpp第 215 行parseCompoundStmt();替代parseStmt();。3.2 现象test1.txt的int a5*32;生成li $t0, 5; mul $t0, $t0, 3; add $t0, $t0, 2结果却是a17而非20原因midCode.cpp的表达式求值顺序错误。5*32应生成t1 5 * 3; t2 t1 2;但原代码按左结合处理先算5*3得t1再t12得t2却把t2当作最终结果赋给a——这本身没错但mips.cpp的genAssign()把t2的值move到a的寄存器时a的寄存器尚未分配registerManager返回空字符串生成move $, $t2非法指令。解决在genAssign()前强制调用regManager.getReg(result)确保目标寄存器已分配。mips.cpp第 132 行加string destReg regManager.getReg(result);。3.3 现象test2.txt包含字符串hello编译时报ERROR_UNTERMINATED_STRING但字符串明明有结尾引号原因quotation.h的parseStringLiteral()函数在检测换行符时用c \n判断但 Windows 文件的换行是\r\n\r被当作普通字符吞掉\n成为字符串内字符导致状态机卡在STATE_STR无法退出。解决quotation.h第 33 行改为if (c \n || c \r)并在word.cpp的getNextChar()中过滤\rif (c \r) continue;。3.4 现象make编译时报undefined reference to Optimizer::optimize()但optimizer.cpp明明存在原因optimizer.h声明了optimize()但optimizer.cpp里实现的是Optimizer::optimize(QuadList)而midCode.cpp调用的是无参版本optimize()。链接时找不到符号。解决统一接口。optimizer.h第 15 行声明void optimize(QuadList ql);midCode.cpp第 88 行调用optimizer.optimize(quadList);optimizer.cpp实现对应版本。3.5 现象gcc编译时报error: to_string is not a member of std原因word.cpp第 195 行用std::to_string(num)但编译器默认 C98 标准to_string是 C11 特性。解决添加编译选项-stdc11或改用sprintfchar buf[16]; sprintf(buf, %d, num); string s(buf);。4. 符号表与类型检查table.h如何把int main()的“类型”焊死在编译期table.h定义了SymbolTable类核心是mapstring, SymbolInfo symbolMap其中SymbolInfo结构体包含typeTYPE_INT/TYPE_VOID、scopeGLOBAL/LOCAL、offset栈偏移等字段。table.h第 52 行enum Type { TYPE_INT, TYPE_VOID, TYPE_CHAR };是类型系统的基石。4.1main函数的类型绑定不是约定是硬编码的初始化table.h第 63 行SymbolTable::initGlobalScope()是关键void SymbolTable::initGlobalScope() { // 硬编码 main 函数 SymbolInfo mainInfo; mainInfo.type TYPE_INT; mainInfo.scope GLOBAL; mainInfo.isFunc true; symbolMap[main] mainInfo; // 关键注册关键字 symbolMap[int].type TYPE_INT; symbolMap[void].type TYPE_VOID; }这里symbolMap[main]的type被设为TYPE_INT意味着任何main函数声明都必须匹配此类型。syntax.cpp的parseFunctionDef()第 92 行检查returnType.type TYPE_INT否则报错。这就是“编译器未包含 main 类型”的根源它不是缺失类型系统而是类型系统只认int main()其他变体void main()、int main(int argc, char* argv[])均被拒绝。4.2 变量类型推导word.cpp不管类型syntax.cpp在赋值时才校验词法分析器word.cpp只负责把int识别为TOKEN_INT不关心int a;的a是什么类型。类型绑定发生在syntax.cpp的parseVarDecl()// syntax.cpp 第 144 行 void Parser::parseVarDecl() { Token typeTok getNextToken(); // int Type type (typeTok.type TOKEN_INT) ? TYPE_INT : TYPE_VOID; Token idTok getNextToken(); // a // 注册到符号表 SymbolInfo info; info.type type; info.scope currentScope; symTable.insert(idTok.value, info); }symTable.insert()调用SymbolTable::insert()table.h第 88 行将a的type设为TYPE_INT。后续a 5;的parseAssignStmt()会检查lhs.type rhs.type若rhs是数字字面量则rhs.type设为TYPE_INT—— 这就是类型兼容性检查的起点。4.3 类型冲突报错error.cpp如何把int a; ahello;拦在编译期parseAssignStmt()syntax.cpp第 178 行在生成赋值 quad 前先调用checkTypeCompatibility(lhs, rhs)bool Parser::checkTypeCompatibility(const SymbolInfo lhs, const SymbolInfo rhs) { if (lhs.type ! rhs.type) { error(Type mismatch: cannot assign typeToString(rhs.type) to typeToString(lhs.type)); return false; } return true; }typeToString()table.h第 102 行把TYPE_INT转成intTYPE_CHAR转成char。当a是TYPE_INT而hello的type是TYPE_CHAR字符串字面量在word.cpp中被标记为TOKEN_STRINGsyntax.cpp将其type设为TYPE_CHAR报错信息就是Type mismatch: cannot assign char to int。注意TOKEN_STRING的type不是TYPE_STRING项目不支持字符串类型而是TYPE_CHAR这是为了简化把字符串视为char数组首地址——但table.h没定义TYPE_STRING所以ahello直接类型不匹配。4.4 符号表作用域{}块如何影响registerManager的寄存器回收SymbolTable支持作用域嵌套。parseCompoundStmt()syntax.cpp第 245 行调用symTable.enterScope()创建新作用域parseStmt()结束时调用symTable.exitScope()。exitScope()不仅清空当前作用域符号还通知registerManager“这些变量死了它们的寄存器可以回收”。registerManager.cpp第 112 行freeRegForScopeExit()遍历regMap删除属于已退出作用域的变量映射并将对应寄存器加入空闲池。这是test2.txt中if{int x1;} x2;不报重定义错的关键——x在if块内分配$t0块结束时$t0被释放外层x2重新分配$t0。5. 验证与调试用spim运行a.out.s并用gdb反向定位词法错误5.1 生成可执行 MIPS三步走通spim流程a.out.s是纯汇编需加载到spimMIPS 模拟器运行。步骤安装 spimUbuntu 下sudo apt install spimmacOS 用brew install spim。补全运行环境a.out.s缺少_start入口和系统调用封装。在a.out.s开头插入.data msg: .asciiz Hello .text .globl _start _start: # 你的生成代码从这里开始 li $v0, 4 # sys_write la $a0, msg # address of string syscall li $v0, 10 # sys_exit syscall运行spim -file a.out.s输入run。若看到Hello说明 MIPS 生成正确。5.2 词法错误反向定位当test1.txt报ERROR_INVALID_OPERATOR如何确定是哪个error.cpp的reportError()输出line: 3, col: 12但col是字符偏移不是可视位置。用vim打开test1.txt执行:set list显示不可见字符再:goto 12跳到第 12 列。更可靠的是在word.cpp的getNextToken()里加日志// word.cpp 第 120 行在 return token 前加 cerr DEBUG: token tokenToString(token.type) at line line , col col endl;编译时加-DDEBUG宏g -DDEBUG -o compiler ...。这样每次getNextToken()都打印位置会被拆成两个TOKEN_ASSIGN第二个TOKEN_ASSIGN的col就是错误源头。5.3 寄存器分配可视化用grep提取a.out.s中的$t使用频次a.out.s里寄存器使用是否均衡执行grep -o \$t[0-7] a.out.s | sort | uniq -c | sort -nr输出类似15 $t0 12 $t1 8 $t2 5 $t3若$t0占比超 70%说明registerManager的 LRU 策略失效可能lruList更新逻辑有 bug需检查getReg()中lruList.remove(var)是否真删掉了旧节点。5.4 类型系统压力测试构造test3.txt验证table.h边界写一个test3.txtint main(){ int a; a 5; if(a 0){ int b; b a * 2; a b; } return a; }编译后检查a.out.s是否有move $t0, $t1a和b分配不同寄存器以及if块内b的寄存器在块结束后是否被复用。这是验证SymbolTable作用域和registerManager生命周期管理的黄金用例。从那以后我每次改syntax.cpp的parseXXX()都强制走一遍test3.txt的寄存器分配可视化再spim跑一次——因为词法分析器的正确性最终要落在$t0有没有被错误覆盖上而不是token.type的枚举值对不对。希望帮到你。本文还有配套的精品资源点击获取