首页 / 资讯中心 / 文章详情

南开编译原理作业实战:简单C语言编译器源码解析与避坑指南

南开编译原理作业实战:简单C语言编译器源码解析与避坑指南 ★ FEATURED ARTICLE
简介这份资源是南开大学软件学院编译原理课程的作业成果包含一个可运行的简单C语言编译器完整源码与配套文档面向计算机、人工智能、通信工程等专业的在校学生及教师也适合作为课程设计、毕业设计或自学编译原理的参考范例。压缩包共50个文件约54KB以18个h头文件、16个cpp源文件、6个c文件为核心辅以lexer.l与grammar.y词法语法定义、Makefile构建脚本、md说明文档及测试用例覆盖词法分析、语法分析、符号表、语法树等编译器关键模块。已有215人学习关注。项目代码均经测试运行成功答辩评审平均分达96分读者可据此理解编译器整体架构与实现细节也可在现有代码基础上修改扩展完成自己的课设或作业任务。1. 从一份编译原理作业说起简单C语言编译器到底能跑通什么很多人搜「南开大学软件学院编译原理作业简单C语言编译器源代码文档说明」真正想知道的不是作业长什么样而是这份东西能不能在自己机器上跑起来、能不能看懂、能不能改成自己的课程设计。我当年带学弟做编译原理实验时最常见的翻车现场就是源码拖下来make一敲满屏红字文档里写的「支持简单C语言子集」到底支持到什么程度没人说得清。所以这篇笔记不聊虚的就按一线做法把这份作业拆开它通常包含词法分析、语法分析、语义检查、中间代码生成这几段配套的源代码和文档说明负责把「怎么编译、怎么测、怎么改」讲清楚。适合正在做编译原理实验的本科生、要交课程设计的人以及想拿一个能跑通的最小C编译器当练手底座的开发者。下面按「先立住原理、再动手复现、最后避坑」的顺序推。2. 先看清这份作业的技术底座词法、语法、语义三段怎么分工2.1 为什么简单C语言编译器大多走「手写递归下降 一遍扫描」编译原理作业里的「简单C语言编译器」绝大多数不是工业级编译器而是教学用的子集编译器。它通常只支持int变量声明、赋值、加减乘除、比较、if、while、return和函数调用不支持结构体、指针运算、预处理宏。选型上常见做法是词法分析手写、语法分析用递归下降、语义分析边解析边做符号表检查整体走一遍扫描。这样做的理由很实际递归下降代码可读性高和文法一一对应助教改作业时一眼能看出你哪条产生式写错了一遍扫描省掉了构造完整AST再遍历的复杂度代码量能压到两三千行以内适合一个人一周内写完。对比另一条路——用 Lex/Yacc 或 Flex/Bison 自动生成——优点是文法改动快缺点是调试时错误定位在生成代码里新手容易懵。南开这份作业如果文档里明确写了「手写」那基本就是递归下降路线。你要做的第一件事不是急着编译而是打开源码目录确认这几个文件是否存在词法分析器常见命名lexer.c/scanner.c、语法分析器parser.c、符号表symtab.c、代码生成codegen.c和主入口main.c。如果文档说明里给了文件清单先对照一遍缺文件就别硬编先找齐。2.2 符号表和作用域编译原理实验里最容易埋雷的地方符号表是这份作业里最容易被低估的模块。简单C语言编译器通常用一个栈式符号表来管理作用域进入{压一层离开}弹一层变量查找从栈顶往下找。常见实现是一个数组加一个scope_level计数器或者链表数组。这里的关键参数是「作用域深度上限」和「符号名最大长度」教学实现里一般写死成 64 或 128。你如果要做扩展比如支持嵌套函数就得先确认符号表是否支持按层查找否则会出现内层变量覆盖外层却查不到的玄学问题。语义检查阶段要做的三件事变量重复声明报错、变量未声明就使用报错、赋值类型不匹配报错。这三条是编译原理实验评分点里出现频率最高的。文档说明里如果写了「语义分析」章节重点看它有没有把这三类错误列出来。没有的话你自己补一个check_semantics函数在解析到标识符时先查符号表查不到就报undeclared identifier查到但类型不符就报type mismatch。这一步做完你的编译器才算「能报错」而不是一遇错就段错误。2.3 中间代码生成三地址码还是直接生成汇编教学用简单C语言编译器在中间代码这块通常有两种选择生成三地址码四元式或者直接生成 x86/伪汇编。三地址码的好处是平台无关、便于做优化实验格式一般是(op, arg1, arg2, result)比如(, a, b, t1)。直接生成汇编的好处是能真正跑起来看到结果但绑定平台换台机器可能就不行。南开这份作业如果文档里提到「目标代码」或「汇编输出」那大概率是后者如果只提到「中间代码」那就是三地址码。我一般建议先跑通三地址码版本确认词法、语法、语义都对了再考虑接一个后端把三地址码翻译成可执行指令。这样调试时你能把每一段源码对应的四元式打出来哪一步错了看得清清楚楚。参数上临时变量命名用t1, t2, t3递增标签用L1, L2递增这是教学实现里最通用的约定文档说明里一般也会沿用。3. 把源代码跑起来从解压到第一个测试用例通过3.1 环境准备与编译命令先确认工具链再动手拿到源代码压缩包后第一步不是make而是看文档说明里的「编译环境」章节。教学编译器常见依赖是gcc、make、flex、bison。如果源码是纯手写递归下降那只需要gcc和make。先跑这三条命令确认环境gcc --version make --version flex --version 2/dev/null || echo no flex, maybe not needed逻辑说明第一条确认 C 编译器存在且版本不过老建议 gcc 7 以上第二条确认 make 可用第三条探测 flex如果文档说用了 Lex 但你没装后面编译会报lex.yy.c找不到。参数上没有特殊要求版本号只要不是十年前的就行。如果make报错说找不到yywrap那是 flex 链接问题在 Makefile 里加-lfl即可。接下来进源码目录先看 Makefile 里的目标名。常见的是make或make all也有写成make compiler的。不要盲目敲make install教学项目一般没有 install 目标敲了只会报错。编译成功后通常会生成一个可执行文件名字可能是compiler、cc或main。用ls -l确认一下然后拿文档里给的示例程序试跑。3.2 用最小测试用例验证词法、语法、语义三段不要一上来就跑复杂程序。先写一个最小用例只包含声明和赋值int main() { int a; a 1 2; return a; }保存为test1.c然后运行编译器。常见调用方式是./compiler test1.c或./compiler test1.c具体看文档说明里的用法。如果输出是三地址码你应该看到类似(, 1, 2, t1) (, t1, _, a) (return, a, _, _)如果输出是汇编你会看到mov、add之类的指令。这一步的目的是确认三段都通了词法能切出int、a、、1、、2语法能识别声明和赋值语句语义能查到a已声明。任何一段挂了输出都会不对。然后逐步加复杂度加if、加while、加函数调用。每加一个特性就跑一次不要攒着一起测。我一般会准备五个测试文件分别覆盖声明赋值、算术表达式、条件分支、循环、函数调用。这样出问题时能快速定位是哪一块没实现。文档说明里如果给了测试用例优先用它的因为评分标准可能和它的预期输出绑定。3.3 参数怎么调临时变量、标签和符号表容量的修改位置教学编译器里几个常改的参数临时变量起始编号、标签起始编号、符号表最大容量、标识符最大长度。这些通常在头文件里用#define定义比如#define MAX_SYMBOLS 128 #define MAX_IDENT_LEN 64 #define TEMP_START 1 #define LABEL_START 1逻辑说明MAX_SYMBOLS控制符号表数组大小测试用例变量多的时候要调大否则会报符号表溢出MAX_IDENT_LEN控制标识符长度一般 64 够用TEMP_START和LABEL_START决定临时变量和标签从几开始编号改成 0 或 1 都行但要和代码生成里的格式化字符串一致。改完这些参数后必须重新make否则不生效。如果你要扩展支持更多语法比如for循环改的是语法分析里的statement函数加一个分支判断for关键字然后递归解析初始化、条件、步进和循环体。这一步没有固定参数靠的是对照文法写代码。文档说明里如果有文法产生式列表照着写基本不会错。4. 避坑与排查编译原理实验里最常见的五类翻车4.1 现象make报错undefined reference to yylex原因词法分析器没链接上解决检查 Makefile 是否包含lexer.o这是 Flex/Bison 路线的经典问题。yylex是词法分析入口如果 Makefile 里只编译了parser.o没编译lexer.o链接阶段就会找不到。解决方法是打开 Makefile确认OBJS变量里包含所有.o文件或者直接make clean make重新来一遍。如果是手写词法分析器检查函数名是否和语法分析器里调用的名字一致常见的是get_token和next_token混用。4.2 现象解析到if语句时段错误原因递归下降里条件表达式解析后没消费右括号解决在match())后加断言递归下降最容易犯的错是「忘了吃掉某个 token」。比如解析if (a b) { ... }时解析完条件表达式后指针停在)上如果没调用match())就继续解析语句块后面全乱。段错误往往是因为指针越界或空指针解引用。解决方法是在每个match后加日志打印当前 token跑一遍就知道卡在哪。参数上建议在match函数里加assert失败时直接报「expected X but got Y」比段错误好查得多。4.3 现象符号表查不到已声明变量原因作用域层级没对齐声明和查找不在同一层解决统一用current_scope变量符号表实现里常见两种写法一种是声明时insert到当前层查找时从当前层往下找另一种是声明和查找都用全局层。如果混用就会出现「明明声明了却查不到」。解决方法是定义一个全局current_scope进入块时current_scope离开时current_scope--insert和lookup都基于这个变量。参数上current_scope初始为 0最大深度一般不超过 16教学用例够用。4.4 现象三地址码里临时变量编号重复原因临时变量计数器没在函数间重置或没递增解决用全局temp_count并在生成时temp_count临时变量编号重复会导致后续优化或翻译时变量冲突。常见原因是temp_count定义成了局部变量每次进函数都从 1 开始。解决方法是把它定义成全局变量或者放在编译器上下文结构体里生成新临时变量时统一走new_temp()函数。参数上temp_count初始值设为 1每调用一次new_temp()就返回t{temp_count}。标签同理用new_label()统一管理。4.5 现象文档说明里的示例输出和实际输出不一致原因源码版本和文档版本不匹配解决以源码为准文档只作参考这是最让人头疼的一类。你按文档里的命令跑输出却对不上可能是文档写的是旧版行为源码已经改了。解决方法是先读源码里的main函数和测试脚本看它实际接受什么参数、输出什么格式。文档说明里的「预期输出」如果和源码不符以源码为准因为评分看的是源码行为。如果文档里给了测试脚本直接跑脚本脚本里的断言才是硬标准。5. 进阶用法把这份作业改成你自己的课程设计5.1 扩展语法加for循环和break的最小改法如果你想让这份简单C语言编译器支持for循环改动集中在语法分析和代码生成两处。语法分析里在statement函数加一个分支if (match(TOKEN_FOR)) { match((); parse_expr(); // 初始化 match(;); parse_expr(); // 条件 match(;); parse_expr(); // 步进 match()); parse_statement(); // 循环体 }逻辑说明这段代码按for (init; cond; step) body的结构顺序解析每个部分复用已有的表达式解析函数。参数上没有新增但要注意for的初始化部分可能为空比如for (; i 10; )所以parse_expr要能处理空表达式。代码生成阶段把for翻译成标签加条件跳转先执行初始化然后打条件标签条件不成立跳结束标签执行循环体执行步进跳回条件标签最后打结束标签。break的实现是在循环体解析时记录当前结束标签遇到break就生成跳转到该标签的指令。5.2 验证方法用差分测试对比你的编译器和 gcc 的行为改完编译器后怎么确认它是对的我一般用差分测试写一批小程序分别用你的编译器和gcc编译运行比较输出。比如for f in tests/*.c; do ./compiler $f out_mine.txt gcc $f -o /tmp/ref /tmp/ref out_ref.txt diff out_mine.txt out_ref.txt || echo MISMATCH: $f done逻辑说明这段脚本遍历tests目录下所有.c文件用你的编译器输出结果用 gcc 编译运行得到参考结果然后 diff。参数上tests目录里放的是只用到你已实现子集的小程序不要放结构体、指针这些没实现的东西。如果 diff 有差异先看是输出格式不同还是计算结果不同。格式不同就统一输出格式结果不同就是语义实现有 bug。5.3 一个具体技巧把符号表和四元式输出成 JSON 方便调试教学编译器输出通常是纯文本调试时不好用程序分析。我习惯加一个--dump-json选项把符号表和四元式输出成 JSONvoid dump_symbols_json(FILE *fp) { fprintf(fp, {\n \symbols\: [\n); for (int i 0; i sym_count; i) { fprintf(fp, {\name\: \%s\, \type\: \%s\, \scope\: %d}%s\n, symbols[i].name, symbols[i].type, symbols[i].scope, i sym_count - 1 ? , : ); } fprintf(fp, ]\n}\n); }逻辑说明这个函数遍历符号表数组把每个符号的名字、类型、作用域层级输出成 JSON 对象。参数上sym_count是当前符号数量symbols是符号数组。加了这个之后你可以用jq或 Python 脚本分析符号表比如统计每个作用域有多少变量、检查有没有重复声明。四元式同理输出成[{op:,arg1:1,arg2:2,result:t1}]这种格式后续写优化 pass 时直接读 JSON 就行。我自己的习惯是每改一个模块就先跑一遍 JSON dump确认中间表示对了再往下走。编译原理实验最怕的就是「看起来能跑但结果不对」JSON 输出能让你在每一步都有据可查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站