简介:这份南京航空航天大学编译原理课程设计资料包面向计算机专业本科生及需要完成编译原理课设的学习者,围绕词法分析、语法分析与符号表管理等核心环节提供一套可参考的完整实现。压缩包共32个文件、约961KB,包含8个txt说明与代码片段、5个exe可执行程序,以及c/cpp/h源码、doc实验报告、ppt答辩材料和obj/pdb等编译输出文件,覆盖从工程配置、源码调试到结果展示的主要流程。已有751人学习,适合在课程设计选题、代码对照和答辩准备阶段查阅。读者可从中获取词法扫描与token识别、语法解析、符号表组织等模块的实现思路,结合课设报告梳理设计目标与测试过程,并参考可执行程序和工程文件复现调试环境;对排查编译错误、理解前端各阶段衔接也具备实际参考价值。
1. 从“能跑就行”到“可验收”:编译原理课程设计真正要交付什么
编译原理课程设计最容易翻车的地方,不是写不出一个递归下降解析器,而是把词法、语法、语义、中间代码拆成四个互不衔接的小实验。到验收时程序能跑,却说不清一条源语句怎样变成四元式,这就不算完整方案。这套课程设计要解决的是前端流水线闭环:给定一个小型语言子集,完成词法切分、语法树构建、符号表与类型检查、三地址码生成,并能用错误用例证明报错定位可信。适合正在做编译原理课程设计的计算机专业学生,也适合需要把课程作业整理成可交付项目的开发者。下面按需求、架构、实现、避坑、验收拆开讲,重点放在能直接照着改的模块边界和测试口径上。
2. 需求与选型:把课程设计拆成四阶段流水线与最小可用子集
2.1 功能边界:词法、语法、语义、中间代码各交付什么
课程设计最怕语言子集越加越大,最后每个阶段都半成品。我一般先定一个能在一周内闭环的文法:整型变量声明、赋值、算术表达式、比较、if/else、while、print、花括号块。这个子集足够展示编译前端核心机制,又不会把代码生成拖成后端大作业。每个阶段的交付物要冻结,否则后面改文法会把词法、语法、语义全部拖下水。
| 阶段 | 输入 | 输出 | 验收证据 |
|---|---|---|---|
| 词法 | 源字符流 | Token 序列,含 kind、value、line、col | 关键字、标识符、数字、注释样例 |
| 语法 | Token 序列 | AST,节点带行列号 | 正常表达式与缺分号样例 |
| 语义 | AST 与符号表 | 标注类型 AST、符号表、语义错误 | 重复声明、未声明、类型不匹配 |
| 中间代码 | 标注 AST | 四元式序列与标签 | 赋值、if、while 三类样例 |
文法文件用 EBNF 写清楚,别只留在脑子里。下面这个片段覆盖了核心优先级和语句形态:
program = { statement } ; statement = decl | assign | if_stmt | while_stmt | print_stmt | block ; decl = "int" ID [ "=" expr ] ";" ; assign = ID "=" expr ";" ; if_stmt = "if" "(" expr ")" statement [ "else" statement ] ; while_stmt = "while" "(" expr ")" statement ; print_stmt = "print" "(" expr ")" ";" ; block = "{" { statement } "}" ; expr = equality ; equality = relational { ( "==" | "!=" ) relational } ; relational = term { ( "<" | ">" | "<=" | ">=" ) term } ; term = factor { ( "+" | "-" ) factor } ; factor = unary { ( "*" | "/" ) unary } ; unary = ( "-" | "!" ) unary | primary ; primary = INT | ID | "(" expr ")" ;这份文法的关键设计是优先级通过分层非终结符固定,*和/比+、-深一层,比较又比等值判断深一层;左结合由循环解析自然实现。悬挂 else 在递归下降里按“else 跟最近一个 if”处理,答辩时能讲清这一条即可。参数上,ID采用字母或下划线开头、后续允许数字和下划线,INT只收十进制整数,注释先支持//行注释。别把词法错误和语法错误混在一个列表里,错误表至少带 phase 字段,验收时一眼能看出是哪一段报的。
2.2 技术选型:手写递归下降 vs 生成器,Python/C++ 怎么取舍
选型不是看哪个工具名气大,而是看答辩时能不能把每一步讲透。课程设计常见的三条路线是 Python 手写递归下降、C++ 手写递归下降、用解析器生成器。三者差别很大,下面这张对比表基本能决定路线:
| 方案 | 开发速度 | 调试可见性 | 答辩解释成本 | 适合场景 |
|---|---|---|---|---|
| Python 手写递归下降 | 快 | 高 | 低 | 需要现场打印 Token、AST、四元式 |
| C++ 手写递归下降 | 中 | 中 | 中 | 课程要求 C++,或想练指针与结构体 |
| 解析器生成器 | 快 | 中 | 高 | 文法复杂、时间紧,但需额外解释生成机制 |
我一般选 Python 手写递归下降。代码短,调试时可以直接把 Token 列表、AST 缩进树、符号表、四元式逐段打印出来,答辩现场改一行输入就能看到输出变化。如果课程明确要求 C++,结构照搬即可:dataclass换struct,list换vector,异常换成错误列表,递归函数形态不变。生成器不是不能用,但课程设计常问“冲突怎么解、优先级怎么落、错误恢复在哪里做”,生成器把这些动作藏起来,容易变成黑匣子,讲不清就被动。
入口参数也要提前定,不要每个阶段各写一个脚本。通常的做法是一个main.py负责参数解析和阶段编排:
python main.py --src tests/cases/assign.src --dump tokens,ast,sym,tac python main.py --src tests/cases/bad_semicolon.src --strict --json--src指定源文件;--dump控制输出哪些阶段,tokens,ast,sym,tac逗号分隔;--strict表示遇到第一个错误就停止;--json给自动测试用,避免脚本解析人类可读文本。这样同一套命令既能演示,也能做回归测试。血泪经验是不要把输出格式改来改去,测试期望文件会跟着崩,最后连自己都不知道哪一版是对的。
2.3 输入输出契约与测试样例设计
课程设计要有“契约感”。我的做法是固定退出码:0 表示编译成功;1 表示词法、语法或语义错误;2 表示参数或文件读取错误。标准输出只放阶段结果,错误统一进错误表,按phase,code,line,col,message输出。这样自动测试只需要看退出码和错误码,不用猜自然语言。
测试样例至少覆盖正常链路和错误链路。下面这组用例足够撑起一次完整验收:
| 用例 | 输入要点 | 期望结果 | 检查点 |
|---|---|---|---|
| assign.src | int a; a = 1 + 2 * 3; | 四元式先乘后加 | 优先级与赋值 |
| if_else.src | if (a < b) { ... } else { ... } | 标签不重复 | 条件跳转 |
| while.src | while (a < 10) { a = a + 1; } | 循环标签成对出现 | 回边 |
| undeclared.src | a = 1; | 语义错误 E302 | 符号表查找 |
| bad_semicolon.src | int a | 语法错误 E201 | 报错行列 |
| comment.src | // note后接赋值 | 注释不产生 Token | 词法跳过 |
期望输出放在tests/expected/下,用 diff 做回归:
python main.py --src tests/cases/if_else.src --dump tokens,ast,sym,tac > tests/actual.out diff -u tests/expected/if_else.out tests/actual.out这一步看似笨,实际上是最便宜的后悔药。只要期望文件稳定,后面改文法、改临时变量命名、改标签生成顺序,都能立刻看到影响范围。测试用例不要只写“能跑”的,错误用例才是答辩时证明你理解编译阶段的材料。
3. 总体架构设计:一次编译流程中的五个模块与两张核心表
3.1 编译流水线:源文件到四元式的数据流
整体结构按“驱动器 + 四个阶段 + 错误表”来分。驱动器读取源文件,调用词法分析得到 Token 流;语法分析消费 Token 流生成 AST;语义分析遍历 AST,填符号表、做类型检查,并把类型信息挂回节点;中间代码生成再遍历标注后的 AST,输出四元式。错误表不参与主数据流,但每个阶段都往里面写记录,最后由驱动器统一输出。
| 阶段 | 主要输入 | 主要输出 | 失败后的行为 |
|---|---|---|---|
| 词法 | 源字符串 | Token 列表 | 记录 E1xx,跳过非法字符继续 |
| 语法 | Token 列表 | AST | 记录 E2xx,按同步集恢复或停止 |
| 语义 | AST、符号表 | 标注 AST、符号表 | 记录 E3xx,尽量继续检查 |
| 中间代码 | 标注 AST | 四元式列表 | 通常不应失败,失败视为内部错误 |
我一般不让语法分析直接算四元式,也不让词法分析顺手做类型判断。AST 是阶段之间的合同,合同清楚,调试才不痛苦。驱动器只做编排,不写业务逻辑;每个阶段返回结果和错误列表,驱动器决定是否继续。词法错误可以继续扫,语法错误在--strict下立刻停,非严格模式下跳到分号或右花括号再继续,这样演示时不会因为一个小错满屏崩。
3.2 符号表与错误表:贯穿前端的两个黑匣子
符号表记录变量名、类型、种类、作用域层级、声明行列和可选的存储偏移。错误表记录阶段、错误码、行列和消息。两张表都要能被打印出来,答辩时把符号表和错误表一展示,很多问题不用解释就清楚了。下面是最小结构:
from dataclasses import dataclass @dataclass class Symbol: name: str # 变量名 type: str # int / bool kind: str # var / func scope: int # 作用域层级,0 为全局 line: int # 声明行 col: int # 声明列 @dataclass class ErrorRecord: phase: str # lexer / parser / semantic code: str # E101 / E201 / E301 line: int col: int message: str作用域用栈实现,进入块压栈,离开块弹栈,查找从内到外。错误码分段:E1xx 给词法,E2xx 给语法,E3xx 给语义。phase字段别省,否则自动测试只能靠 message 猜错误来源。scope从 0 开始,全局变量在 0 层,块内变量逐层加一。这个结构不复杂,但它是后面所有检查的依据。
3.3 目录结构与构建入口
目录不要平铺一堆脚本,按阶段分文件,答辩时打开目录就能讲架构:
compiler/ main.py # 参数解析与阶段编排 lexer.py # Token 与词法分析 parser.py # AST 与递归下降 semantic.py # 符号表、类型检查 tac.py # 四元式生成 errors.py # 错误记录与格式化 tests/ cases/ # 源文件用例 expected/ # 期望输出 docs/ grammar.md # 文法说明 README.md # 运行方式与验收清单main.py只做三件事:读参数、按顺序调用阶段、格式化输出。业务逻辑留在各自模块里,这样单测可以只测词法或只测语义。演示机器上尽量别引入重依赖,标准库能完成就别装第三方包;如果用了,就在requirements.txt锁版本。路径一律相对项目根目录,别写死本机绝对路径,否则答辩换一台机器就翻车。
4. 核心模块实现:手写词法分析器、递归下降语法分析与三地址码生成
4.1 词法分析:DFA 状态迁移与 Token 结构
词法分析器先固定 Token 结构,再写扫描循环。关键字不要用一堆if硬判,先按标识符扫完,再查关键字表;运算符按“最长匹配”先看双字符,再看单字符。下面这段实现覆盖空白、行注释、标识符、整数、双字符运算符和单字符运算符:
from dataclasses import dataclass KEYWORDS = {"int", "if", "else", "while", "print"} @dataclass class Token: kind: str # KW / ID / INT / OP / EOF / ERROR value: str line: int col: int class Lexer: def __init__(self, src: str): self.src, self.i, self.line, self.col = src, 0, 1, 1 def next_token(self): # 跳过空白;行列号由 _advance 统一维护 while self.i < len(self.src) and self.src[self.i].isspace(): self._advance(self.src[self.i]) # 行注释:一直跳到换行,不产生 Token if self.i < len(self.src) and self.src[self.i:self.i + 2] == "//": while self.i < len(self.src) and self.src[self.i] != "\n": self._advance(self.src[self.i]) return self.next_token() if self.i >= len(self.src): return Token("EOF", "", self.line, self.col) ch = self.src[self.i] start_line, start_col = self.line, self.col if ch.isalpha() or ch == "_": buf = [] while self.i < len(self.src) and (self.src[self.i].isalnum() or self.src[self.i] == "_"): buf.append(self.src[self.i]) self._advance(self.src[self.i]) word = "".join(buf) return Token("KW" if word in KEYWORDS else "ID", word, start_line, start_col) if ch.isdigit(): buf = [] while self.i < len(self.src) and self.src[self.i].isdigit(): buf.append(self.src[self.i]) self._advance(self.src[self.i]) return Token("INT", "".join(buf), start_line, start_col) # 双字符运算符优先,保证 <= 不会被拆成 < 和 = for op in ("==", "!=", "<=", ">="): if self.src.startswith(op, self.i): self._advance(op[0]) self._advance(op[1]) return Token("OP", op, start_line, start_col) if ch in "+-*/=<>(){};": self._advance(ch) return Token("OP", ch, start_line, start_col) self._advance(ch) return Token("ERROR", ch, start_line, start_col) def _advance(self, ch): self.i += 1 if ch == "\n": self.line += 1 self.col = 1 else: self.col += 1这段扫描器的关键点在两个地方。第一,start_line和start_col在进入每条分支前保存,返回 Token 时用的是起始位置,不是扫描结束后的位置;报错定位准不准,基本就看这里。第二,关键字识别放在标识符扫描之后,intx会作为一个 ID,不会被拆成int加x。kind的取值固定为 KW、ID、INT、OP、EOF、ERROR,测试脚本只认这几个值,别中途改名字。line和col从 1 开始,和编辑器显示保持一致,答辩时对照源码行号不容易错位。
4.2 语法分析:递归下降与 AST 构建
语法分析用递归下降,按表达式优先级分层写函数。AST 节点统一成kind + value + children,后面语义分析和代码生成都按 kind 分派。下面给的是语句与算术表达式的核心骨架:
from dataclasses import dataclass, field @dataclass class Node: kind: str value: str | None = None children: list = field(default_factory=list) line: int = 0 col: int = 0 class Parser: def __init__(self, tokens): self.toks, self.pos = tokens, 0 def peek(self, k=0): return self.toks[min(self.pos + k, len(self.toks) - 1)] def eat(self, kind=None, value=None): t = self.peek() if kind and t.kind != kind: raise SyntaxError(f"E201 期望 {kind},实际 {t.value} @{t.line}:{t.col}") if value and t.value != value: raise SyntaxError(f"E202 期望 {value},实际 {t.value} @{t.line}:{t.col}") self.pos += 1 return t def parse_program(self): stmts = [] while self.peek().kind != "EOF": stmts.append(self.parse_stmt()) return Node("Program", children=stmts) def parse_stmt(self): t = self.peek() if t.kind == "KW" and t.value == "int": self.eat("KW", "int") name = self.eat("ID") init = None if self.peek().value == "=": self.eat("OP", "=") init = self.parse_expr() self.eat("OP", ";") return Node("Decl", name.value, [init] if init else [], t.line, t.col) if t.kind == "ID": name = self.eat("ID") self.eat("OP", "=") expr = self.parse_expr() self.eat("OP", ";") return Node("Assign", name.value, [expr], t.line, t.col) if t.kind == "KW" and t.value == "print": self.eat("KW", "print") self.eat("OP", "(") expr = self.parse_expr() self.eat("OP", ")") self.eat("OP", ";") return Node("Print", children=[expr], line=t.line, col=t.col) raise SyntaxError(f"E203 无法识别语句 {t.value} @{t.line}:{t.col}") def parse_expr(self): left = self.parse_term() while self.peek().value in ("+", "-"): op = self.eat("OP").value left = Node("Bin", op, [left, self.parse_term()], left.line, left.col) return left def parse_term(self): left = self.parse_primary() while self.peek().value in ("*", "/"): op = self.eat("OP").value left = Node("Bin", op, [left, self.parse_primary()], left.line, left.col) return left def parse_primary(self): t = self.peek() if t.kind == "INT": self.eat("INT") return Node("Num", t.value, line=t.line, col=t.col) if t.kind == "ID": self.eat("ID") return Node("Var", t.value, line=t.line, col=t.col) if t.value == "(": self.eat("OP", "(") expr = self.parse_expr() self.eat("OP", ")") return expr raise SyntaxError(f"E204 期望表达式,实际 {t.value} @{t.line}:{t.col}")eat同时检查 kind 和 value,错误信息里带行列号,方便直接映射到源码。parse_expr和parse_term用循环处理左结合,parse_primary处理括号、整数和变量。if/while 的写法沿用同一套路:吃掉if、左括号,解析条件表达式,吃掉右括号,再递归解析语句;如果有else,再解析 else 分支。节点上的line和col不是装饰,语法错误和语义错误都靠它定位到原始源码。完整骨架里把 if/while 补上后,AST 打印用缩进递归输出,答辩时一眼能看出树形结构。
4.3 语义检查与符号表填充
语义阶段做三件事:声明入表、使用查表、类型检查。重复声明、未声明使用、类型不匹配都要给出错误码。下面是最小可运行的检查框架:
@dataclass class Symbol: name: str type: str scope: int line: int class Semantic: def __init__(self): self.scopes = [{}] # 栈顶为当前作用域 self.errors = [] def declare(self, name, typ, line): if name in self.scopes[-1]: self.errors.append(f"E301 重复声明 {name} @{line}") return self.scopes[-1][name] = Symbol(name, typ, len(self.scopes) - 1, line) def lookup(self, name): for scope in reversed(self.scopes): if name in scope: return scope[name] return None def check_expr(self, node): if node.kind == "Num": return "int" if node.kind == "Var": sym = self.lookup(node.value) if sym is None: self.errors.append(f"E302 未声明 {node.value} @{node.line}") return "error" return sym.type if node.kind == "Bin": left = self.check_expr(node.children[0]) right = self.check_expr(node.children[1]) if left != "int" or right != "int": self.errors.append(f"E303 算术运算需要 int @{node.line}") return "error" return "int" return "error" def check(self, node): if node.kind == "Decl": self.declare(node.value, "int", node.line) if node.children: self.check_expr(node.children[0]) elif node.kind == "Assign": if self.lookup(node.value) is None: self.errors.append(f"E304 赋值前未声明 {node.value} @{node.line}") self.check_expr(node.children[0]) for child in node.children: if child is not None: self.check(child)scopes是列表套字典,进入块时 append 空字典,离开块时 pop,查找从后往前。declare只查当前作用域,允许内层遮蔽外层;lookup从内到外找,找不到返回 None。错误码 E301 到 E304 覆盖重复声明、未声明使用、算术类型错误和赋值前未声明。条件表达式通常返回 bool,如果语言子集只保留 int,就在 if/while 条件处单独检查“必须是比较表达式”,别把所有表达式都硬塞成 int。
4.4 中间代码生成:从 AST 到三地址码
中间代码用四元式,字段是操作符、两个源操作数、结果。表达式递归生成临时变量,语句按类型发射四元式。赋值统一用=,打印用print,条件跳转用ifFalse和goto。下面是最小生成器:
@dataclass class Quad: op: str a1: str = "" a2: str = "" res: str = "" class TAC: def __init__(self): self.quads = [] self.tmp = 0 self.label = 0 def new_tmp(self): self.tmp += 1 return f"t{self.tmp}" def new_label(self): self.label += 1 return f"L{self.label}" def emit(self, op, a1="", a2="", res=""): self.quads.append(Quad(op, a1, a2, res)) def gen_expr(self, node): if node.kind == "Num": return node.value if node.kind == "Var": return node.value if node.kind == "Bin": left = self.gen_expr(node.children[0]) right = self.gen_expr(node.children[1]) temp = self.new_tmp() self.emit(node.value, left, right, temp) return temp raise ValueError(f"未知表达式节点 {node.kind}") def gen_stmt(self, node): if node.kind == "Decl": if node.children: value = self.gen_expr(node.children[0]) self.emit("=", value, "", node.value) elif node.kind == "Assign": value = self.gen_expr(node.children[0]) self.emit("=", value, "", node.value) elif node.kind == "Print": value = self.gen_expr(node.children[0]) self.emit("print", value, "", "")new_tmp和new_label是唯一入口,临时变量和标签都从 1 开始递增,避免嵌套 if 时标签重名。gen_expr只处理算术,比较表达式在条件生成里单独处理:先算左右操作数,发射比较四元式,再根据真假标签发射ifFalse或goto。赋值四元式用(=, value, _, name),打印用(print, value, _, _)。中间代码不追求优化,追求结构清楚、标签成对、临时变量不覆盖,答辩时能顺着四元式复述源语句的执行顺序就够了。
5. 避坑与排查:课设里最容易翻车的五类问题
5.1 词法歧义与关键字识别
现象是intx被拆成关键字int和标识符x,或者<=被拆成<和=,后面语法分析报一堆莫名其妙的错。原因通常有两个:关键字匹配没有等标识符完整扫描完再做,运算符没有按最长匹配优先。解决方法是先扫完整标识符,再查关键字表;双字符运算符放在单字符前面判断;注释在词法阶段整体跳过,不产生 Token。测试时专门加intx、a<=b、x//note三类样例,把 Token 列表打印出来对照,比在语法阶段猜错误快得多。
5.2 语法错误恢复与报错定位
现象是少一个分号后,后面每一行都报错,错误表刷屏,真正的问题被淹没。原因是语法分析遇到错误直接抛异常,或者没有同步集,解析器在原地打转。解决方法是给语句级同步集:遇到错误跳到下一个分号、右花括号或文件结束,记录第一条错误后继续;--strict模式才立即停止。报错信息必须带期望 Token、实际 Token 和行列号,别只写“语法错误”。答辩时演示一个缺分号样例,再演示一个括号不匹配样例,能看出恢复策略是否可靠。
5.3 语义阶段空指针与类型检查
现象是语义分析遍历 AST 时突然崩,或者未声明变量被当成 int 继续生成代码。原因通常是节点字段不统一,有的子节点是 None,有的表达式没有返回类型;符号表查找失败后没有返回错误类型。解决方法是所有节点统一用kind + children,可选子节点显式判空;lookup找不到时返回 None 并记录 E302,后续类型检查遇到error类型就短路,避免连锁报错。作用域进出要成对,块解析完必须 pop,否则内层变量泄漏到外层,重复声明检查会失真。
5.4 中间代码临时变量与标签管理
现象是嵌套 if 生成两个同名标签,或者临时变量t1被后面的表达式覆盖,四元式看起来对但执行顺序错。原因多是标签和临时变量用了局部计数器,或者生成条件时先发射跳转再算条件。解决办法是统一由new_tmp和new_label发号,任何模块都不许手写t1、L1;条件生成按“先算条件、再发射假跳转、再生成分支体、最后补 goto 和标签”的顺序走。生成完打印四元式,检查每个标签都有定义、每个临时变量只赋值一次,基本能排除大部分问题。
5.5 测试用例覆盖与演示环境
现象是本地跑得好好的,答辩机器上缺依赖、路径找不到、输出格式对不上。原因是硬编码绝对路径、依赖没锁版本、期望输出没有纳入版本管理。解决办法是入口只用相对路径,测试用例和期望输出一起提交;依赖能省就省,必须用的写进requirements.txt;准备一条一键脚本,按顺序跑正常用例、错误用例和 diff。演示前在干净目录里从零跑一遍,别等现场才试。这个环节没有技术含量,但最容易翻车,提前十分钟能省掉一堆解释。
6. 验收与答辩:用一条编译链路证明方案闭环
6.1 答辩现场按一条编译链路演示
验收时不要从目录结构开始念,直接拿一条源语句走完整链路。我一般选int a; a = 1 + 2 * 3; print(a);,先展示 Token 流,再展示 AST,再展示符号表和四元式。评委能看到2 * 3先出t1,1 + t1再出t2,最后赋值和打印,这条链路一走,优先级、临时变量、符号表全讲清楚了。然后用错误用例补一刀:缺分号看语法错误定位,未声明变量看语义错误码,注释样例看词法跳过。正常链路证明能跑,错误链路证明能诊断,两边都成立才算闭环。
| 验收项 | 演示动作 | 通过标准 |
|---|---|---|
| 词法 | 输入含关键字、标识符、双字符运算符的样例 | Token 类型和行列号正确 |
| 语法 | 输入嵌套括号和算术表达式 | AST 层级体现优先级 |
| 语义 | 输入重复声明、未声明使用 | 错误码和行号准确 |
| 中间代码 | 输入 if/else 和 while | 标签成对、临时变量不重名 |
| 回归 | 跑测试脚本并 diff | 期望输出无差异 |
| 环境 | 在干净目录重新运行 | 不依赖本机绝对路径 |
6.2 两个必备材料和一个小技巧
材料一是文法说明,把支持和不支持的语法写清楚,别让评委替你猜边界;材料二是测试清单,正常用例、错误用例、期望输出各一组。小技巧是准备“最小失败样例”:一个缺分号、一个未声明、一个重复声明,每个只改一行,现场演示时能快速说明错误恢复和错误码。答辩前把--dump输出固定下来,别现场改格式;如果评委临时加一条语句,先判断它
本文还有配套的精品资源,点击获取