ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

东南大学编译原理实验:从零手搓编译器全流程实战

东南大学编译原理实验:从零手搓编译器全流程实战 简介这份资源是东南大学软件学院编译原理课程实验项目的完整实现面向正在学习编译原理、需要动手实践编译器各阶段的高校学生与自学者。它围绕词法分析、语法分析、语义分析、中间代码生成、目标代码生成与代码优化等完整流程构建了一个从源代码到可执行代码的编译器模拟系统帮助读者把课堂理论落到可运行的工程代码上。压缩包共28个文件以12个java源文件和12个class编译产物为主体另含2个txt说明、1个iml工程配置与1个md文档整体约20KB体量轻便便于快速导入IDE阅读与调试。目录中可见词法分析器、语法分析器等模块划分结构清晰适合按阶段对照学习。目前已有63人学习下载。读者可借此理解各编译阶段的衔接方式、AST构建思路与代码优化切入点并参考工程组织方式完成自己的课程实验是编译原理实践环节一份可直接复用的参考实现。1. 从零手搓编译器东南大学软件学院编译原理实验到底在练什么很多人第一次看到“编译原理课程实验”这几个字脑子里浮现的是龙书里那些晦涩的自动机推导和课后习题答案。但东南大学软件学院这套实验项目的真正价值在于它逼着你把一个完整的编译器从前到后走一遍——从词法分析、语法分析、语义分析到中间代码生成、目标代码优化一个环节都不能少。你写的不再是零散的算法题而是一个能跑通“源代码进、可执行代码出”的模拟系统。这件事对新手来说门槛不低对熟手来说却是一次难得的体系化梳理。如果你正在搜“编译原理实验怎么做”“java编译原理怎么结合”或者被课后习题答案折磨得头大那这篇笔记就是给你写的。我会按实际动手的顺序把每个阶段的选型理由、核心代码、参数设置和踩坑记录讲清楚让你能照着复现也能看到边界在哪。2. 词法分析与语法分析从字符流到语法树的落地路径2.1 词法分析器为什么建议手写而不是直接上 Lex很多同学第一反应是用 Flex/Lex 自动生成词法分析器觉得省事。但东南大学这套实验的评分点往往落在“你是否理解正则表达式到 DFA 的转换过程”直接调库反而拿不到过程分。我一般会建议手写一个基于状态转移的词法分析器核心逻辑不超过 200 行但能把标识符、关键字、运算符、界符、常量全部覆盖。先定义 Token 类型和 Token 结构# token_types.py # 定义所有词法单元类型方便后续语法分析引用 TOKEN_TYPES { KEYWORD: KEYWORD, # int, if, while, return 等 IDENTIFIER: IDENTIFIER, # 变量名、函数名 NUMBER: NUMBER, # 整数、浮点数 OPERATOR: OPERATOR, # - * / ! DELIMITER: DELIMITER, # ; , ( ) { } EOF: EOF # 文件结束标记 } class Token: def __init__(self, type_, value, line, col): self.type type_ # Token 类型 self.value value # 原始字符串值 self.line line # 所在行号报错用 self.col col # 所在列号报错用 def __repr__(self): return fToken({self.type}, {self.value!r}, line{self.line})逻辑说明Token 类里保留 line 和 col 是为了后续语法分析报错时能精确定位。很多同学只存 value结果语法错误时只能报“第某行附近有错”调试成本翻倍。参数说明TOKEN_TYPES 用字典而不是枚举是为了后续扩展方便比如加注释类型、字符串类型时直接加键值对即可。接下来是词法分析器主体采用逐字符扫描加状态机的方式# lexer.py from token_types import Token, TOKEN_TYPES KEYWORDS {int, float, if, else, while, return, void} OPERATORS {, -, *, /, , , !, , , , } DELIMITERS {;, ,, (, ), {, }} class Lexer: def __init__(self, source): self.source source self.pos 0 # 当前字符位置 self.line 1 # 当前行号 self.col 1 # 当前列号 self.tokens [] # 结果 Token 列表 def tokenize(self): while self.pos len(self.source): ch self.source[self.pos] if ch in \t\r: self._advance() elif ch \n: self.line 1 self.col 1 self.pos 1 elif ch.isalpha() or ch _: self._read_identifier() elif ch.isdigit(): self._read_number() elif ch in OPERATORS or ch in DELIMITERS: self._read_operator_or_delimiter() else: raise SyntaxError(f非法字符 {ch!r} 在 {self.line}:{self.col}) self.tokens.append(Token(EOF, , self.line, self.col)) return self.tokens def _advance(self): self.pos 1 self.col 1 def _read_identifier(self): start self.pos while self.pos len(self.source) and (self.source[self.pos].isalnum() or self.source[self.pos] _): self._advance() word self.source[start:self.pos] token_type KEYWORD if word in KEYWORDS else IDENTIFIER self.tokens.append(Token(token_type, word, self.line, self.col - len(word))) def _read_number(self): start self.pos while self.pos len(self.source) and self.source[self.pos].isdigit(): self._advance() # 处理小数 if self.pos len(self.source) and self.source[self.pos] .: self._advance() while self.pos len(self.source) and self.source[self.pos].isdigit(): self._advance() self.tokens.append(Token(NUMBER, self.source[start:self.pos], self.line, self.col)) def _read_operator_or_delimiter(self): ch self.source[self.pos] # 优先匹配双字符运算符 if self.pos 1 len(self.source): two self.source[self.pos:self.pos2] if two in OPERATORS: self.tokens.append(Token(OPERATOR, two, self.line, self.col)) self.pos 2 self.col 2 return token_type OPERATOR if ch in OPERATORS else DELIMITER self.tokens.append(Token(token_type, ch, self.line, self.col)) self._advance()逻辑说明_read_operator_or_delimiter里先检查双字符运算符如、再回退到单字符这是手写词法分析器最容易翻车的地方——如果先匹配单字符会被拆成两个语法分析直接崩。参数说明self.line和self.col在换行时重置保证报错位置准确。KEYWORDS 集合建议按实验要求补全常见的是 C 语言子集。2.2 递归下降语法分析用 LL(1) 把 Token 流变成 AST语法分析阶段东南大学实验通常要求实现递归下降分析器或 LL(1) 分析表。我建议手写递归下降因为代码结构直接对应文法产生式调试时能一眼看出哪条规则出了问题。先定义 AST 节点# ast_nodes.py class ASTNode: pass class Program(ASTNode): def __init__(self, declarations): self.declarations declarations # 顶层声明列表 class FunctionDecl(ASTNode): def __init__(self, return_type, name, params, body): self.return_type return_type self.name name self.params params self.body body class BinaryOp(ASTNode): def __init__(self, op, left, right): self.op op self.left left self.right right class NumberLiteral(ASTNode): def __init__(self, value): self.value value class Identifier(ASTNode): def __init__(self, name): self.name name逻辑说明AST 节点只存结构信息不存 Token 位置位置信息在语义分析阶段通过符号表关联。这样 AST 更干净后续遍历也更快。参数说明FunctionDecl里的params是参数列表每个参数包含类型和名字建议用元组(type, name)存储。递归下降分析器核心代码# parser.py from ast_nodes import * from token_types import Token class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): return self.tokens[self.pos] def consume(self, expected_typeNone, expected_valueNone): token self.tokens[self.pos] if expected_type and token.type ! expected_type: raise SyntaxError(f期望 {expected_type}实际 {token.type} 在 {token.line}:{token.col}) if expected_value and token.value ! expected_value: raise SyntaxError(f期望 {expected_value!r}实际 {token.value!r} 在 {token.line}:{token.col}) self.pos 1 return token def parse_program(self): declarations [] while self.peek().type ! EOF: declarations.append(self.parse_declaration()) return Program(declarations) def parse_declaration(self): # 简化处理只支持函数声明 return_type self.consume(KEYWORD).value name self.consume(IDENTIFIER).value self.consume(DELIMITER, () params [] while self.peek().value ! ): ptype self.consume(KEYWORD).value pname self.consume(IDENTIFIER).value params.append((ptype, pname)) if self.peek().value ,: self.consume(DELIMITER, ,) self.consume(DELIMITER, )) body self.parse_block() return FunctionDecl(return_type, name, params, body) def parse_block(self): self.consume(DELIMITER, {) stmts [] while self.peek().value ! }: stmts.append(self.parse_statement()) self.consume(DELIMITER, }) return stmts def parse_statement(self): token self.peek() if token.value return: self.consume(KEYWORD, return) expr self.parse_expression() self.consume(DELIMITER, ;) return (return, expr) elif token.type IDENTIFIER: name self.consume(IDENTIFIER).value self.consume(OPERATOR, ) expr self.parse_expression() self.consume(DELIMITER, ;) return (assign, name, expr) else: raise SyntaxError(f无法识别的语句在 {token.line}:{token.col}) def parse_expression(self): left self.parse_term() while self.peek().value in (, -): op self.consume(OPERATOR).value right self.parse_term() left BinaryOp(op, left, right) return left def parse_term(self): left self.parse_factor() while self.peek().value in (*, /): op self.consume(OPERATOR).value right self.parse_factor() left BinaryOp(op, left, right) return left def parse_factor(self): token self.peek() if token.type NUMBER: self.consume(NUMBER) return NumberLiteral(token.value) elif token.type IDENTIFIER: self.consume(IDENTIFIER) return Identifier(token.value) elif token.value (: self.consume(DELIMITER, () expr self.parse_expression() self.consume(DELIMITER, )) return expr else: raise SyntaxError(f无法识别的表达式在 {token.line}:{token.col})逻辑说明parse_expression和parse_term分层处理加减和乘除天然实现了运算符优先级。parse_factor处理括号和原子表达式递归下降的结构一目了然。参数说明consume方法支持同时校验类型和值比如consume(KEYWORD, return)确保当前 Token 既是关键字又是return。这种双重校验在调试时能快速定位是词法错了还是语法错了。3. 语义分析与中间代码生成符号表、类型检查和四元式输出3.1 符号表怎么设计才能同时支持作用域和类型检查语义分析阶段最容易踩的坑是符号表设计得太简单导致嵌套作用域里变量重名时直接覆盖。我一般用栈式符号表每个作用域一层进入块时压栈退出时弹栈。# symbol_table.py class Symbol: def __init__(self, name, type_, kind, scope_level): self.name name self.type type_ # int, float, void self.kind kind # variable, function, parameter self.scope_level scope_level class SymbolTable: def __init__(self): self.scopes [{}] # 栈式作用域初始全局作用域 self.current_level 0 def enter_scope(self): self.scopes.append({}) self.current_level 1 def exit_scope(self): if self.current_level 0: raise RuntimeError(无法退出全局作用域) self.scopes.pop() self.current_level - 1 def declare(self, name, type_, kind): if name in self.scopes[-1]: raise SemanticError(f变量 {name} 在当前作用域重复声明) self.scopes[-1][name] Symbol(name, type_, kind, self.current_level) def lookup(self, name): # 从当前作用域向外逐层查找 for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(f未声明的标识符 {name}) def lookup_current(self, name): return self.scopes[-1].get(name)逻辑说明lookup从最内层作用域向外查找符合大多数语言的名称解析规则。declare只检查当前作用域是否重复允许内层遮蔽外层同名变量。参数说明scope_level记录声明时的作用域层级后续做类型检查时可以用来判断变量是否在合法作用域内被引用。语义分析器主体# semantic.py from symbol_table import SymbolTable, SemanticError from ast_nodes import * class SemanticAnalyzer: def __init__(self): self.symtab SymbolTable() self.errors [] def analyze(self, node): method visit_ node.__class__.__name__ visitor getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node): raise SemanticError(f没有为 {node.__class__.__name__} 定义 visit 方法) def visit_Program(self, node): for decl in node.declarations: self.analyze(decl) def visit_FunctionDecl(self, node): # 函数名加入全局作用域 self.symtab.declare(node.name, node.return_type, function) self.symtab.enter_scope() for ptype, pname in node.params: self.symtab.declare(pname, ptype, parameter) for stmt in node.body: self.analyze(stmt) self.symtab.exit_scope() def visit_BinaryOp(self, node): left_type self.analyze(node.left) right_type self.analyze(node.right) if left_type ! right_type: raise SemanticError(f类型不匹配{left_type} 和 {right_type} 在运算符 {node.op}) return left_type def visit_NumberLiteral(self, node): return int if . not in node.value else float def visit_Identifier(self, node): symbol self.symtab.lookup(node.name) return symbol.type逻辑说明用visit_前缀加类名的方式做分发新增 AST 节点时只需加对应方法不用改主流程。visit_BinaryOp里做类型检查左右类型不一致直接报错。参数说明errors列表用于收集多个错误后统一输出而不是遇到第一个错误就退出这样用户能一次性看到所有问题。3.2 四元式中间代码生成从 AST 到线性指令序列中间代码生成阶段东南大学实验通常要求输出四元式。四元式结构是(op, arg1, arg2, result)比如(, a, b, t1)表示t1 a b。# ir_generator.py from ast_nodes import * class Quadruple: def __init__(self, op, arg1, arg2, result): self.op op self.arg1 arg1 self.arg2 arg2 self.result result def __repr__(self): return f({self.op}, {self.arg1}, {self.arg2}, {self.result}) class IRGenerator: def __init__(self): self.quads [] self.temp_count 0 def new_temp(self): self.temp_count 1 return ft{self.temp_count} def generate(self, node): method gen_ node.__class__.__name__ visitor getattr(self, method, self.generic_gen) return visitor(node) def generic_gen(self, node): raise RuntimeError(f没有为 {node.__class__.__name__} 定义 gen 方法) def gen_Program(self, node): for decl in node.declarations: self.generate(decl) return self.quads def gen_FunctionDecl(self, node): self.quads.append(Quadruple(func, node.name, len(node.params), None)) for stmt in node.body: self.generate(stmt) self.quads.append(Quadruple(endfunc, node.name, None, None)) def gen_BinaryOp(self, node): left self.generate(node.left) right self.generate(node.right) temp self.new_temp() self.quads.append(Quadruple(node.op, left, right, temp)) return temp def gen_NumberLiteral(self, node): return node.value def gen_Identifier(self, node): return node.name逻辑说明gen_BinaryOp递归生成左右子表达式的代码然后分配临时变量存放结果。临时变量命名用t1、t2递增保证唯一性。参数说明temp_count是全局计数器每次new_temp递增。如果实验要求临时变量按作用域重置可以在gen_FunctionDecl里保存和恢复temp_count。对于赋值语句和返回语句需要单独处理def gen_assign(self, name, expr): # 赋值语句(assign, name, expr) result self.generate(expr) self.quads.append(Quadruple(, result, None, name)) def gen_return(self, expr): result self.generate(expr) self.quads.append(Quadruple(return, result, None, None))逻辑说明赋值语句把表达式结果直接写入变量名返回语句生成return四元式。这两个方法在gen_FunctionDecl遍历语句时根据语句类型调用。参数说明四元式的result字段在赋值时是变量名在返回时是None后续目标代码生成阶段根据op区分处理。4. 目标代码优化与生成从四元式到可执行模拟指令4.1 常量折叠与公共子表达式消除两个必做的优化 pass目标代码优化阶段最基础也最容易被实验评分点覆盖的是常量折叠和公共子表达式消除。常量折叠在四元式层面做遍历所有四元式如果arg1和arg2都是数字直接计算结果并替换。# optimizer.py from ir_generator import Quadruple class Optimizer: def __init__(self, quads): self.quads quads def constant_folding(self): new_quads [] for quad in self.quads: if quad.op in (, -, *, /) and self._is_number(quad.arg1) and self._is_number(quad.arg2): # 两个操作数都是常量直接计算 result self._compute(quad.op, quad.arg1, quad.arg2) new_quads.append(Quadruple(, result, None, quad.result)) else: new_quads.append(quad) self.quads new_quads return self.quads def _is_number(self, s): try: float(s) return True except (ValueError, TypeError): return False def _compute(self, op, a, b): a, b float(a), float(b) if op : return str(int(a b)) if a b int(a b) else str(a b) if op -: return str(int(a - b)) if a - b int(a - b) else str(a - b) if op *: return str(int(a * b)) if a * b int(a * b) else str(a * b) if op /: return str(int(a / b)) if a / b int(a / b) else str(a / b)逻辑说明constant_folding遍历四元式列表遇到可折叠的表达式直接计算结果生成一条赋值四元式。_is_number用异常捕获判断字符串是否为数字比正则更简洁。参数说明_compute里对结果做整数判断如果结果是整数值就输出整数形式避免t1 3.0这种不美观的输出。公共子表达式消除需要先做可用表达式分析简化版可以用字典记录已计算的表达式def common_subexpression_elimination(self): expr_map {} # (op, arg1, arg2) - result_temp new_quads [] for quad in self.quads: if quad.op in (, -, *, /): key (quad.op, quad.arg1, quad.arg2) if key in expr_map: # 已经计算过直接用之前的临时变量 new_quads.append(Quadruple(, expr_map[key], None, quad.result)) else: expr_map[key] quad.result new_quads.append(quad) else: new_quads.append(quad) self.quads new_quads return self.quads逻辑说明expr_map以(op, arg1, arg2)为键记录第一次计算时的结果临时变量。后续遇到相同表达式时直接生成赋值四元式避免重复计算。参数说明这个简化版没有考虑变量被重新赋值后表达式失效的情况适合实验级别的优化。如果要更严谨需要在赋值语句处清空expr_map中涉及该变量的条目。4.2 目标代码生成把四元式翻译成栈式模拟指令目标代码生成阶段常见做法是生成一种简单的栈式虚拟机指令每条指令对应一个操作。指令集可以设计为PUSH、LOAD、STORE、ADD、SUB、MUL、DIV、RET、CALL。# codegen.py class CodeGenerator: def __init__(self, quads): self.quads quads self.instructions [] self.var_offset {} # 变量名 - 栈偏移 self.next_offset 0 def get_offset(self, name): if name not in self.var_offset: self.var_offset[name] self.next_offset self.next_offset 1 return self.var_offset[name] def generate(self): for quad in self.quads: self.gen_quad(quad) return self.instructions def gen_quad(self, quad): op quad.op if op func: self.instructions.append(fFUNC {quad.arg1} {quad.arg2}) elif op endfunc: self.instructions.append(fENDFUNC {quad.arg1}) elif op : # 赋值把 arg1 的值存入 result if self._is_number(quad.arg1): self.instructions.append(fPUSH {quad.arg1}) else: self.instructions.append(fLOAD {self.get_offset(quad.arg1)}) self.instructions.append(fSTORE {self.get_offset(quad.result)}) elif op in (, -, *, /): # 二元运算先压左操作数再压右操作数然后运算 if self._is_number(quad.arg1): self.instructions.append(fPUSH {quad.arg1}) else: self.instructions.append(fLOAD {self.get_offset(quad.arg1)}) if self._is_number(quad.arg2): self.instructions.append(fPUSH {quad.arg2}) else: self.instructions.append(fLOAD {self.get_offset(quad.arg2)}) instr {: ADD, -: SUB, *: MUL, /: DIV}[op] self.instructions.append(instr) self.instructions.append(fSTORE {self.get_offset(quad.result)}) elif op return: if self._is_number(quad.arg1): self.instructions.append(fPUSH {quad.arg1}) else: self.instructions.append(fLOAD {self.get_offset(quad.arg1)}) self.instructions.append(RET) def _is_number(self, s): try: float(s) return True except (ValueError, TypeError): return False逻辑说明gen_quad根据四元式类型生成对应指令。二元运算先生成两个LOAD或PUSH再生成运算指令最后STORE结果。get_offset为每个变量分配唯一的栈偏移。参数说明var_offset字典在函数级别应该重置如果实验要求支持多函数需要在FUNC指令处保存和恢复偏移映射。5. 避坑与排查编译原理实验里最容易翻车的五个地方5.1 词法分析阶段双字符运算符被拆成两个单字符现象输入a b词法分析输出Token(OPERATOR, )、Token(OPERATOR, )语法分析报“意外的运算符”。原因_read_operator_or_delimiter里先匹配了单字符没有优先检查双字符组合。解决在匹配单字符之前先检查self.source[self.pos:self.pos2]是否在 OPERATORS 集合里。如果是消费两个字符并生成一个 Token。5.2 语法分析阶段左递归导致无限递归现象解析表达式时程序卡死或栈溢出。原因文法写成E - E T | T递归下降分析器直接调用parse_expression会无限递归。解决消除左递归改写成E - T EE - T E | ε。或者用循环代替递归如parse_expression里用while处理运算符。5.3 语义分析阶段符号表作用域没有正确弹栈现象内层块声明的变量在外层块被错误引用或者内层块退出后变量仍然可见。原因enter_scope和exit_scope没有成对调用或者exit_scope在异常路径上被跳过。解决用try/finally确保exit_scope一定执行。或者在 AST 遍历时进入块节点时压栈离开时弹栈不要依赖异常处理。5.4 中间代码生成阶段临时变量命名冲突现象两个不同的表达式生成了同名的临时变量导致后续优化和代码生成出错。原因temp_count在递归生成时被重置或者多个函数共享同一个计数器但没有隔离。解决temp_count作为实例变量全局递增不要在任何地方重置。如果实验要求按函数隔离在gen_FunctionDecl入口保存当前值出口恢复。5.5 目标代码生成阶段变量偏移分配不一致现象同一个变量在LOAD和STORE时使用了不同的偏移运行时取到错误的值。原因get_offset在两次调用之间被重置或者变量名大小写不一致导致字典键不匹配。解决var_offset字典在代码生成器实例化时创建整个生成过程不重置。变量名统一转小写或保持原样不要混用。6. 进阶技巧用解释器验证目标代码的正确性写完代码生成器后最直接的验证方式是写一个栈式虚拟机解释器逐条执行生成的指令看结果是否和预期一致。这个解释器不需要太复杂能处理PUSH、LOAD、STORE、ADD、SUB、MUL、DIV、RET就够了。# vm.py class StackVM: def __init__(self): self.stack [] self.vars {} # 变量偏移 - 值 self.pc 0 # 程序计数器 self.instructions [] self.return_value None def load(self, instructions): self.instructions instructions self.pc 0 def run(self): while self.pc len(self.instructions): instr self.instructions[self.pc] parts instr.split() op parts[0] if op PUSH: self.stack.append(float(parts[1])) elif op LOAD: self.stack.append(self.vars.get(int(parts[1]), 0.0)) elif op STORE: self.vars[int(parts[1])] self.stack.pop() elif op ADD: b, a self.stack.pop(), self.stack.pop() self.stack.append(a b) elif op SUB: b, a self.stack.pop(), self.stack.pop() self.stack.append(a - b) elif op MUL: b, a self.stack.pop(), self.stack.pop() self.stack.append(a * b) elif op DIV: b, a self.stack.pop(), self.stack.pop() self.stack.append(a / b) elif op RET: self.return_value self.stack.pop() break self.pc 1 return self.return_value逻辑说明StackVM维护一个操作数栈和一个变量表。PUSH把常量压栈LOAD从变量表取值压栈STORE弹栈存入变量表。二元运算弹两个操作数计算后压回结果。参数说明vars字典的键是变量偏移整数值是浮点数。如果实验要求支持整数可以在PUSH和STORE时做类型转换。验证流程可以写成一个端到端的测试脚本# test_end_to_end.py from lexer import Lexer from parser import Parser from semantic import SemanticAnalyzer from ir_generator import IRGenerator from optimizer import Optimizer from codegen import CodeGenerator from vm import StackVM source int main() { int a 3 4 * 2; return a; } # 1. 词法分析 tokens Lexer(source).tokenize() print(Token 流:, tokens) # 2. 语法分析 ast Parser(tokens).parse_program() # 3. 语义分析 SemanticAnalyzer().analyze(ast) # 4. 中间代码生成 quads IRGenerator().generate(ast) print(四元式:, quads) # 5. 优化 optimizer Optimizer(quads) quads optimizer.constant_folding() quads optimizer.common_subexpression_elimination() print(优化后四元式:, quads) # 6. 目标代码生成 instructions CodeGenerator(quads).generate() print(目标指令:, instructions) # 7. 虚拟机执行 vm StackVM() vm.load(instructions) result vm.run() print(执行结果:, result) # 预期输出 11.0逻辑说明这个脚本把七个阶段串起来每一步的输出都打印出来方便定位问题。如果最终结果不对可以从后往前逐阶段检查。参数说明source里的表达式3 4 * 2预期结果是 11如果输出不是 11说明优先级处理或常量折叠有问题。我自己的习惯是每写完一个阶段就先跑这个阶段的单元测试不要等全部写完再联调。编译原理实验的调试成本很高一个词法错误可能导致语法分析报一堆莫名其妙的错逐阶段验证能省下大量时间。另外符号表和临时变量的命名规则最好在动手前就定好中途改命名规则会让所有阶段的代码都要跟着改。希望帮到你。本文还有配套的精品资源点击获取
返回列表