☰
JavaCC实现类C编译器:编译原理课程设计完整实战指南
2026/10/3 2:50:55 网站建设 项目流程

简介:面向计算机专业学生及编译原理课程设计者的完整资料包,来自重庆理工大学,基于Java与JavaCC实现一个类C语言编译器。资源覆盖从文法设计、词法分析、递归下降语法分析到结果输出与验证的完整流程,并额外使用Python编写LL1算法,通过教材实例验证准确无误。压缩包共380个文件,约3.02MB,主要包含源码、语法定义文件、编译生成类文件、测试样例、运行输出结果及课程设计报告所需脚本,目录清晰,便于按模块查阅与二次开发。资源内置自动化脚本,可一键执行词法分析、语法分析以及Basic、Mixed多组结果测试,并自动整理输出到指定路径;同时提供可视化栈演示,帮助理解函数调用时内存空间变化。目前已有857人学习下载,适合正在完成编译原理课程设计、需要完整可运行参考方案的同学。

1. 拿到“重庆理工大学编译原理课程设计 java javacc 类C编译器”这个题目,先别急着抄代码

这个标题看起来是一行课程设计题目,实际上是一整套完整的编译原理实验要求:用 Java 语言、借助 JavaCC 工具,实现一个类 C 语言的编译器。第一次接触的人最容易踩的坑,是把重点放在“复现某个开源编译器”上,而忽略了这门课真正要考察的东西——词法分析、语法分析、语义分析和代码生成,每一步都要能在你提交的源码里被看到、被运行、被验证。

这个项目能解决的核心问题很明确:把编译原理教材里的理论,落到一个能跑的最小编译器上。它适合正在做编译原理课程设计的学生,也适合想快速体验 JavaCC 工作流的 Java 开发者。下面这套做法,是我把类 C 子集编译器从零搭起来的完整路径,包含 .jj 文法的设计、符号表与四元式的实现、以及我在调试过程中翻车最多的地方。

2. 先拆需求再动手:类C编译器要交什么、跑什么、怎么被评判

2.1 把课程设计拆成四个能独立验收的模块

类 C 编译器不是非要支持完整的 C 语言,课设题目里“类C”三个字给了你很大的裁剪空间。我一般会先划出四层,每一层都有独立的交付物,这样既好分工,也方便老师验收:

模块实现方式交付物
词法分析JavaCC 的 TOKEN / SKIP 规则token 种类定义
语法分析JavaCC 的 production(产生式)文法文件(.jj)
语义分析手写符号表 + 类型检查Symbol 类、作用域栈
代码生成手写遍历 AST 生成四元式 / 目标指令可执行的输出结果

最容易被忽略的是第一模块和第三模块之间的接口。JavaCC 默认只做词法 + 语法分析,它不会自动构建 AST,也不会帮你维护符号表。所以你的 .jj 文件里每个 production 都要显式返回一个 AST 节点,或者直接在一遍扫描时顺带做语义动作。课程设计阶段我建议前者——先建 AST,再单独遍历生成代码,逻辑更清楚,调试时也好定位问题。

2.2 准备环境:javacc.jar 和一条能跑通的命令

环境准备只需要 JDK 和 javacc.jar 两样东西。JDK 配置属于标配,确保java和javac命令在终端里可用——这里容易卡住的就是环境变量配置问题,JAVA_HOME 指到 JDK 安装目录、PATH 里加上%JAVA_HOME%\bin,Windows 和 Linux 都一样。

javacc.jar 从官网或 Maven 仓库拿,不需要安装,它就是一个可执行的 jar。下面是我常用的目录结构:

# 项目目录规划 mkdir -p compiler/src/main/javacc # 放 .jj 源文件 mkdir -p compiler/src/main/java/cc # 放生成的 Java 代码 mkdir -p compiler/src/main/java/ast # 放手写的 AST 节点 mkdir -p compiler/test/cases # 放测试用的 .c 文件

把 javacc.jar 放进项目根目录后,用命令行生成解析器:

# 从 .jj 生成 Java 解析器代码 java -cp javacc.jar javacc \ -OUTPUT_DIRECTORY:src/main/java/cc \ src/main/javacc/C.jj

这里有两个参数需要说明。-OUTPUT_DIRECTORY指定生成的 Java 文件输出路径,目录必须存在,否则命令直接失败;.jj文件路径是最后一个参数,javacc 会读取它并生成C.java(解析器)、CTokenManager.java(词法分析器)、Token.java等文件。生成的这些文件不要手工改,因为只要你重新执行 javacc,它们就会被覆盖——.jj才是唯一的源码,这是我第一次做课设时才明白的“后悔药”逻辑。

2.3 跑通第一个最小 .jj 文件

先别急着写完整文法,用一个只识别“一个标识符后跟文件结束符”的最小示例,验证工具链是通的。.jj 文件的结构分为三段:PARSER_BEGIN/PARSER_END 包裹的类体、TOKEN 定义、以及语法产生式。

// src/main/javacc/C.jj options { STATIC = false; // 生成非静态解析器,便于多次实例化 } PARSER_BEGIN(C) package cc; import java.io.*; public class C { public static void main(String[] args) throws ParseException { C parser = new C(new InputStreamReader(System.in)); parser.program(); System.out.println("parse ok"); } } PARSER_END(C) // 跳过空白与换行 SKIP : { " " | "\t" | "\n" | "\r" } // 标识符:字母开头,后跟字母或数字 TOKEN : { < ID: (["A"-"Z","a"-"z"]) (["A"-"Z","a"-"z","0"-"9"])* > } // 语法:入口产生式 void program() : {} { <ID> <EOF> }

这个文件的逻辑很直白:SKIP声明哪些字符序列在词法分析时直接丢弃,TOKEN声明标识符的正则规则,program()是一个产生式,要求输入流是“一个标识符后紧跟文件结束符”。生成后用 javac 编译全部 Java 文件,再运行java cc.C,输入hello就会看到parse ok,输入hello world就会抛出 ParseException。

选项里STATIC = false值得专门说一句。JavaCC 默认生成静态解析器,所有方法都是 static,这在单次调用时没问题,但如果你想在一个测试类里反复创建解析器解析多个文件,静态成员会互相污染状态。课程设计阶段建议一律设成 false,代价只是生成的代码里有几个实例方法而已。

3. 用 JavaCC 写类C子集的词法与文法:一份能运行的 .jj 长什么样

3.1 token 设计:关键字、标识符和字面量怎么不打架

类 C 语言的关键字一般选int、char、if、else、while、for、return、void这八个就够。很多人写 .jj 时会把关键字像标识符一样定义成TOKEN,结果发现输入if时被识别成<ID>。这是因为 JavaCC 的词法规则有一个隐性优先级:匹配长度相同的 token 时,先定义的规则优先。

TOKEN : { < INT: "int" > | < CHAR: "char" > | < VOID: "void" > | < IF: "if" > | < ELSE: "else" > | < WHILE: "while" > | < FOR: "for" > | < RETURN:"return" > } TOKEN : { < ID: (["A"-"Z","a"-"z"]) (["A"-"Z","a"-"z","0"-"9","_"])* > } TOKEN : { < NUM: (["0"-"9"])+ > | < CHAR_LIT: "'" (~["'","\\","\n","\r"]) "'" > }

关键字必须声明在<ID>之前,这是 JavaCC 使用者的血泪经验。否则int这个输入会被<ID>规则完整匹配,而<INT>规则根本轮不到。把关键字放在前面之后,JavaCC 遇到int时会优先返回<INT>token。

数字和字符字面量同样放在后面。CHAR_LIT的正则里,~["'","\\","\n","\r"]表示“除单引号、反斜杠、换行、回车之外的任意字符”,注意反斜杠必须写成"\\",因为 .jj 文件本身也是 Java 字符串的转义规则。如果后面要支持转义字符(如'\n'),这个正则还需要扩展。

3.2 表达式文法:用层级产生式解决优先级与结合性

表达式是语法分析里最容易写崩的地方。C 语言的运算符优先级有十几层,课设不必全做,实现+ - * / %、比较运算、逻辑与和逻辑或就够。JavaCC 不像 yacc 有%left声明优先级,它只能靠产生式的嵌套来体现优先级。

// 最低优先级:逻辑或 void expression() : {} { logical_or_expression() } void logical_or_expression() : {} { logical_and_expression() ( "||" logical_and_expression() )* } void logical_and_expression() : {} { equality_expression() ( "&&" equality_expression() )* } void equality_expression() : {} { relational_expression() ( ("==" | "!=") relational_expression() )* } void relational_expression() : {} { additive_expression() ( ("<" | ">" | "<=" | ">=") additive_expression() )* } // 加减 void additive_expression() : {} { multiplicative_expression() ( ("+" | "-") multiplicative_expression() )* } // 乘除模 void multiplicative_expression() : {} { unary_expression() ( ("*" | "/" | "%") unary_expression() )* } void unary_expression() : {} { ( "!" | "-" ) unary_expression() | primary_expression() } void primary_expression() : {} { <NUM> | <CHAR_LIT> | <ID> | "(" expression() ")" }

这个写法有两点值得展开。第一,优先级从低到高逐层包裹,expression()在最外层,越往下运算符优先级越高,unary_expression()用递归处理单目运算符,primary_expression()落到原子。第二,左结合通过(op term)*这个循环实现——每遇到一个运算符就再解析一个右侧操作数,生成的 AST 是左结合的。这里最忌讳的是照抄教科书里的左递归写法:

// 千万不要这样写 void additive_expression() : {} { additive_expression() "+" multiplicative_expression() }

JavaCC 生成的是递归下降解析器,递归下降天然不支持左递归。上面这个产生式会让解析器在additive_expression()里无限调用自身,运行起来直接抛 StackOverflowError。把左递归改写为循环是必须的,这不是风格问题,是能不能跑起来的问题。

3.3 语句与函数:支撑课设验收的最小语法集合

语句的语法相对固定,if、while、for和return是标配。书写时注意每个产生式都要能返回 AST 节点,至少也要返回一个布尔值表示是否匹配成功。下面是语句产生式的核心片段:

void statement() : {} { <ID> "=" expression() ";" | <IF> "(" expression() ")" statement() ( <ELSE> statement() )? | <WHILE> "(" expression() ")" statement() | <RETURN> expression()? ";" | block_statement() } void block_statement() : {} { "{" ( statement() )* "}" } void local_declaration() : {} { ( <INT> | <CHAR> ) <ID> ( "=" expression() )? ";" }

这里有个细节容易忽略:( <ELSE> statement() )?表示 else 分支是可选的,这在 LL(1) 文法里是经典的“悬空 else”冲突点。JavaCC 遇到这种可选分支,默认选择“匹配最近的 if”,这个行为和 C 语言一致,所以大多数情况下不用额外处理。但如果你在课设报告里宣称自己处理了悬空 else,就要在文法里显式区分“内层语句不允许是裸 if”,否则老师会拿嵌套 if 的测试用例来验证你的解析结果。

函数定义和函数调用也是必须的,否则没法写递归程序做验证。函数定义在入口产生式里:

void program() : {} { ( function_definition() )* <EOF> } void function_definition() : {} { ( <INT> | <CHAR> | <VOID> ) <ID> "(" formal_parameters()? ")" block_statement() } void formal_parameters() : {} { ( <INT> | <CHAR> ) <ID> ( "," ( <INT> | <CHAR> ) <ID> )* } void function_call() : {} { <ID> "(" actual_parameters()? ")" }

( function_definition() )* <EOF>让 program() 接受零个或多个函数定义。注意 formal_parameters 用了?和*处理可选参数和多个参数,这些符号的含义和正则一致。写完这些,你的 .jj 就已经能解析一个包含函数、循环、条件、局部变量的类 C 子集了——语法分析这一关就算过了。

4. 语义分析与代码生成:从语法树到能跑的结果

4.1 符号表:变量去哪了、类型对不对

语法分析只回答“输入合不合文法”,不回答“变量x有没有声明”“x + 1里的x是 int 还是 char”。这些是语义分析要解决的。符号表是最朴素的做法,一个 HashMap 加一个作用域栈就能支撑课设需求。

// ast/Symbol.java package ast; public class Symbol { public String name; // 变量名 public String type; // int / char public int offset; // 相对栈帧基址的偏移 public int size; // 占字节数,int=4, char=1 public Symbol(String name, String type, int offset) { this.name = name; this.type = type; this.size = type.equals("int") ? 4 : 1; this.offset = offset; } }

作用域用栈来维护,进入一个{}块时压入一层新表,退出时弹掉。查变量时从栈顶往栈底找,这正好对应 C 语言的“内层屏蔽外层”规则。

// ast/ScopeStack.java public class ScopeStack { private java.util.Stack<java.util.HashMap<String, Symbol>> stack; public ScopeStack() { stack = new java.util.Stack<>(); stack.push(new java.util.HashMap<>()); // 全局作用域 } public void enterScope() { stack.push(new java.util.HashMap<>()); } public void exitScope() { stack.pop(); } public void declare(String name, Symbol symbol) { if (stack.peek().containsKey(name)) { throw new RuntimeException("变量重复声明: " + name); } stack.peek().put(name, symbol); } public Symbol lookup(String name) { for (int i = stack.size() - 1; i >= 0; i--) { Symbol symbol = stack.get(i).get(name); if (symbol != null) return symbol; } throw new RuntimeException("未声明的变量: " + name); } private int nextOffset = 0; public int allocOffset() { int off = nextOffset; nextOffset += 4; return off; } }

这个实现里值得说明的是allocOffset():每个新变量分配一个递增的偏移量,代码生成时用它计算变量在栈帧里的位置。课设阶段不需要做寄存器分配,栈式分配最简单、出错率最低。类型检查可以放在declare和生成表达式的代码里:赋值语句要求左右两侧类型一致,return的类型要和函数返回类型一致,这些全部通过Symbol.type字符串比较完成。

4.2 从 AST 到四元式:中间代码的生成本质是拼接字符串

如果不想在 .jj 里塞太多动作,可以单独做一遍 AST 遍历来生成四元式。四元式用一条记录表示一个运算,字段是op, arg1, arg2, result。最简单的实现就是四个字段的类,输出时拼成字符串。

// codegen/Quad.java package codegen; public class Quad { public String op; // 运算符,如 ADD、SUB、JMP、LABEL public String arg1; // 操作数1 public String arg2; // 操作数2,可空 public String result;// 结果 public Quad(String op, String arg1, String arg2, String result) { this.op = op; this.arg1 = arg1; this.arg2 = arg2; this.result = result; } @Override public String toString() { return op + " " + (arg1 == null ? "_" : arg1) + " " + (arg2 == null ? "_" : arg2) + " " + (result == null ? "_" : result); } }

表达式a + b * 3翻译成四元式的过程是递归的:先算b * 3得到临时变量t1,再算a + t1得到t2。翻译 if 语句时用回填——先记录跳转指令所在的编号,等 then 分支翻译完再补上目标标签。

// codegen/ExprTranslator.java 片段 public String translateExpr(ExprNode node) { if (node instanceof BinOpNode) { BinOpNode bin = (BinOpNode) node; String left = translateExpr(bin.left); String right = translateExpr(bin.right); String tmp = newTemp(); // 生成 t1, t2, t3... String op = mapOp(bin.op); // "+" -> "ADD" quads.add(new Quad(op, left, right, tmp)); return tmp; } if (node instanceof NumNode) { return String.valueOf(((NumNode) node).value); } if (node instanceof VarNode) { return ((VarNode) node).name; } throw new RuntimeException("无法翻译的表达式节点"); }

newTemp()内部只做一个计数器递增,每次返回"t" + (++counter)。这个翻译规则的核心思路是:每个中间节点生成一个临时变量,把子表达式的结果存入临时变量,再输出一条四元式。等全部翻译完,整个quads列表就是中间代码。表达式a + b * 3会输出类似这样的序列:

MUL b 3 t1 ADD a t1 t2

有了四元式,后面无论是生成汇编还是虚拟机指令,都只需要做机械的指令映射,不需要再关心表达式的树结构。

4.3 目标代码:做 MIPS 还是做自定义虚拟机

课程设计的最后一个环节是把四元式翻译成可执行的东西。两条路线二选一:生成 MIPS 汇编交给 SPIM 模拟器运行,或者设计一套极简的自定义虚拟机指令,再写一个解释器执行。MIPS 路线更“正统”,但要处理寄存器分配和栈帧约定,工作量明显更大;自定义虚拟机只需要设计十几条指令,解释器两百行就能写完,而且测试起来非常直观。

对比项MIPS 汇编自定义虚拟机
验收观感专业,能跑 SPIM直观,能看指令流
工作量寄存器分配、栈帧、系统调用指令设计 + while 循环解释器
调试难度看汇编和寄存器看指令和栈内容即可
课设建议时间充裕且想冲高分两周内能稳交付

我通常推荐自定义虚拟机。指令集简单到 12 条就够:LOAD_CONST、LOAD_VAR、STORE_VAR、ADD、SUB、MUL、DIV、JMP、JMP_IF_FALSE、CALL、RET、HALT。四元式到虚拟机的映射是理想中的一对一:ADD a b t1翻译成ADD指令,操作数换成变量的内存地址。解释器就一个while (true)循环,读取指令、执行、更新 PC,看到HALT就结束。

// vm/VirtualMachine.java 片段 public void run() { while (true) { int op = code[pc++]; switch (op) { case ADD: { int a = stack[stack[pc++]]; int b = stack[stack[pc++]]; int d = stack[pc++]; stack[d] = a + b; break; } case HALT: return; // 其余指令照此扩展 } } }

这段解释器代码的要点是:栈上既存变量值,也存变量地址。ADD指令的三个操作数都是地址,解释器先从栈里按地址取出两个值,相加后写回目标地址。这种“地址栈”设计比直接存值更容易映射四元式里的临时变量,因为四元式的每个临时变量本质上就是一个“虚拟地址”。课程设计做到这一步,整个编译器已经能输出并执行程序,剩下的就是系统性测试和准备答辩演示。

5. 课设避坑指南:5 个让我翻车的细节

5.1 左递归导致无限递归:StackOverflowError 还是 javacc 卡死

现象:把表达式文法按照教科书“expr → expr + term”的形式写进 .jj,执行 javacc 时生成成功,但运行解析器解析任意带表达式的输入,程序瞬间抛出 StackOverflowError;严重时连生成阶段都会报错。

原因:JavaCC 生成的是递归下降解析器,每个产生式对应一个 Java 方法。左递归产生式会让expression()方法在入口处直接调用自身,永远没有机会消费任何 token,栈一层层积累直到溢出。

解决:所有二元运算都要改写成term (op term)*的形式,用一个循环替代左递归。检查方法很简单:看你的 .jj 里有没有一个产生式的方法名出现在自己右侧的第一个位置,如果有,就是左递归。我见过不少同学把additive_expression的左递归已经改对了,却在写unary_expression时又写了unary_expression : unary_expression "+" ...,这类排查要逐个产生式过。

5.2 JavaCC 报 choice conflict:警告不处理,运行时行为会“玄学”

现象:执行 javacc 生成代码时,控制台打出类似Warning: Choice conflict in (...)的警告,后面跟着两个产生式的名字。程序有时候能跑,有时候解析同样的输入却报错,看起来毫无规律。

原因:JavaCC 默认用 LL(1) 判断,即只看一个 token 决定走哪个分支。如果两个分支的 FIRST 集合有交集,JavaCC 就会提示 choice conflict,并默认选择先声明的分支,这个选择未必是你想要的。

解决:优先改写文法,提取公共前缀,让两个分支的第一个 token 不同;改写不了时,在冲突点前加显式 LOOKAHEAD,例如( LOOKAHEAD(2) <ELSE> statement() )?,告诉 JavaCC 向前看两个 token 决策。注意 LOOKAHEAD 是“报警后”的手段,不是一开始就到处加的——加太多会拖慢解析速度,而且掩盖文法本身的问题。课设报告里写明哪些地方用了 LOOKAHEAD、为什么用,这反而是加分项。

5.3 中文注释乱码:.jj 和测试用例的编码必须统一

现象:类 C 源码里写了中文注释,词法分析时要么报非法字符,要么注释里的中文变成乱码后污染了后面的 token。

原因:问题出在两个地方。第一,javacc 生成解析器代码时,默认按平台编码读取 .jj 文件,Windows 下可能是 GBK,Linux 下可能是 UTF-8;第二,生成的解析器读取输入源文件时,用的是FileReader这类默认编码的 Reader,和你的源码编码不一致就会出现乱码。

解决:第一处在生成时显式指定编码,命令行加-ENCODING:UTF-8;第二处在构造解析器时自己包一层 Reader,例如new InputStreamReader(new FileInputStream(file), "UTF-8")。建议整个项目的 .jj、.java、测试 .c 文件全部统一为 UTF-8,并且把编码参数写进生成脚本里,不要依赖系统默认值——这是最不显眼但也最能拉开交付质量差距的细节。

5.4 悬空 else 的归属:JavaCC 默认行为和你以为的不一样

现象:输入if (a) if (b) x = 1; else y = 2;,有的同学预期 else 匹配外层 if,但运行自己的编译器后,发现 else 匹配的是内层 if。

原因:这是编译原理里最经典的二义性。JavaCC 对可选分支的处理策略是“贪心匹配”,( <ELSE> statement() )?这个可选结构在遇到 else 时倾向于消费它,因此 else 默认归最近的 if。

解决:C 语言的真实行为就是“else 匹配最近未匹配的 if”,所以如果你的目标是类 C,保持 JavaCC 的默认行为反而是对的。真正要处理的是在文法层面对“内层语句”做限制:内层 if 的 then 分支不允许出现一个裸的 if 语句作为单语句,或者干脆在课设文档里写明“不支持这种嵌套写法”,测试用例里避开即可。最怕的是两种都没做,老师现场输入嵌套 if 程序,你的编译器给出的结果和预期不符。我的做法是:保留默认行为,然后在测试用例里加一条嵌套 if 的案例,输出里注明 else 绑定关系,让验收方看到你是清楚这个语义的。

5.5 提交物组织混乱:只交生成的 Java 文件等于没交源码

现象:答辩时老师打开你的项目,看到几十个生成的 Java 文件——C.java、CTokenManager.java、TokenMgrError.java——翻了半天找不到文法定义,最后只能问“你的语法规则在哪”。

原因:JavaCC 的工作方式决定了生成代码不是人该去维护的东西,真正的源文件只有.jj一个。但很多同学的提交方式就是把整个 src 目录打包,让验收方在几百行生成的 token 管理代码里找设计意图,这非常减分。

解决:按下面这个结构提交,每个文件都有明确职责:

  • src/main/javacc/C.jj:词法与语法规则的唯一来源,修改编译行为只改这个文件
  • src/main/java/ast/*.java:AST 节点类,定义表达式、语句、函数等结构
  • src/main/java/codegen/*.java:符号表、四元式生成、代码生成
  • src/main/java/vm/*.java:虚拟机解释器
  • src/main/java/cc/C.java:入口 main 方法,负责读文件、调解析器、输出结果
  • test/cases/*.c:测试用例,每个用例配一个.out预期输出文件
  • build.sh或run.bat:一键生成、编译、运行脚本

这样做还有一个好处:老师如果要看你的文法设计,只需要打开一个几百行的.jj文件,而不是去翻生成代码。如果答辩要演示修改文法后的效果,直接改.jj再跑脚本,一分钟内能看到改动生效,这个流畅度在演示时非常加分。

6. 验证与调试技巧:如何让别人信服你的编译器真的能跑

验证一个编译器,不能只跑一个1 + 2就结束。我建议准备三组测试用例:第一组是正常程序,包含变量声明、赋值、四则运算、if 和 while,跑出正确结果;第二组是边界输入,比如嵌套 20 层的括号表达式、大整数加法、空函数体,用来检验解析器会不会栈溢出、词法分析会不会丢字符;第三组是非法输入,故意写未声明变量、类型不匹配的赋值、缺少分号的语句,预期行为是编译器打印清晰的报错信息,而不是抛出一串 Java 异常栈。第三组最容易被忽略,但也最能让验收方确认你的语义分析真的做了。

调试解析过程时,一个非常趁手的技巧是打开解析器的 tracing 开关。JavaCC 生成的解析器类自带enable_tracing()和disable_tracing()方法,在 main 方法里调用后再解析,会在标准输出上打印每一个被匹配的 token 和进入的产生式方法名。比如输入if (a) b = 1;,trace 会显示出statement进入、匹配<IF>、匹配(、匹配<ID> a……这时如果出现某个产生式被不预期地跳过了,你能直接定位到文法冲突。如果嫌修改代码麻烦,也可以在生成时给 javacc 加-DEBUG_PARSER:true参数,生成的代码默认就带 trace 输出。

进阶方面,我给一个能写进课设报告里的亮点:常量折叠。在四元式生成阶段,每生成一条二元运算四元式之前,先检查两个操作数是否都是数字字面量,如果是,就直接算出结果,不再生成这条四元式。比如a = 2 + 3 * 4;直接翻译成a = 14的一轮赋值,既减少了中间代码量,又能展示你对优化的理解。实现时只需要在translateExpr里加一个字符串转数字的判断,整个改动不超过三十行,但答辩时可以展开讲“这是编译原理里数据流分析的雏形”。

最后说一个我的习惯:所有中间代码和虚拟机输出,我都保留了“可观测”的打印入口,四元式列表、指令流、栈变化都能单独开关。这让我在调错时从黑匣子变成了白盒子,也让答辩现场随时能把一个输入到输出的完整过程展示出来。希望这套从 .jj 到虚拟机、从测试用例到答辩演示的路径,能帮你在做这个课设时少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询