主要题型(根据 23 级考题)
- NFA、DFA 画图、转换、最小 DFA
- 画语法树,找短语、简单短语和句柄
- 求三个集合和递归下降程序编写
- 驻留法求符号表
- 写四元式并优化它
NFA,DFA 和正则表达式
概念题
与DFA相比,NFA的非确定性体现在:
A. 允许有多个开始状态
B. 允许有多个终止状态
C. 在没有任何输入的情况下允许进行状态转换
D. 一个状态可以有多个不同后继状态
选择 AC,注意 D 说的应该是在同一个输入下,一个状态可以有多个不同的后继状态。
正则表达式和有限自动机的关系以下说法正确的是:
A. 一个正则表达式只能等价于一个确定的有限自动机
B. 正则表达式、NFA和DFA在接受语言的能力上是相互等价的
C. 对任何形式正则表达式r,都存在一个NFA M,满足L(M)=L®
D. 一个正则表达式可以转化为一个等价的自动机,但自动机不一定都能表示为等价的正则表达式
选择 BC。
A,一个正则表达式可以对应多个等价的有限自动机。比如同一个正则表达式可以先构造出一个 NFA,再转成 DFA,还可以继续最小化 DFA;这些自动机结构可能不同,但接受的语言相同。题干说“只能等价于一个确定的有限自动机”,把“语言等价”误解成了“结构唯一”。
B,正则表达式、NFA、DFA 都接受正则语言,所以识别能力相同。NFA 可以通过子集构造转成 DFA,正则表达式也可以构造出等价自动机。
C,对任意正则表达式 r,都可以构造一个 NFA M,使得 L(M)=L(r),这是正则表达式到自动机转换的基本结论。
D,自动机也可以转换为等价的正则表达式。
正则表达式、NFA、DFA 是同一类语言的三种表示法。
正则表达式⟺NFA⟺DFA
文法相关概念
文法一般写成:
G=(VN,VT,S,P)
其中:
| 概念 | 含义 | 解释 |
|---|
| VN | 非终结符集合 | 可以继续展开的“语法类别”,如 E,T,F |
| VT | 终结符集合 | 最终出现在程序里的基本符号,如 id,+,∗,(,) |
| S | 开始符号 | 推导的起点,且 S∈VN |
| P | 产生式集合 | 规定如何展开非终结符 |
文法符号全集常写作:
V=VN∪VT,VN∩VT=∅
终结符从语法分析角度看是语言中不可再分的基本符号,通常对应词法分析输出的 Token 类型。
产生式、终结符、非终结符
上下文无关文法中的产生式一般写成:
A→α
其中:
A∈VN
α∈(VT∪VN)∗
A 是产生式左部,也叫头;α 是产生式右部,也叫体;→ 或 ::= 读作“定义为”“推导出”。例如:
E→E+T
T→F
F→i
这里 E,T,F 是非终结符,+,i 是终结符。
文法的分类
文法分类按乔姆斯基层次来记,层级从强到弱是:0 型 ⊃ 1 型 ⊃ 2 型 ⊃ 3 型
| 类型 | 名称 | 产生式形式 | 对应语言 | 对应模型 |
|---|
| 0 型 | 无限制文法 / 短语文法 | α→β,且 α 中含非终结符 | 递归可枚举语言 | 图灵机 |
| 1 型 | 上下文有关文法 | αAβ→αγβ,通常要求长度增长或保持 | 上下文有关语言 | 线性有界自动机 |
| 2 型 | 上下文无关文法 | A→α | 上下文无关语言 | 下推自动机 |
| 3 型 | 正则文法 | 右线性 A→aB 或 A→a;左线性 A→Ba 或 A→a | 正则语言 | 有限自动机 |
编译原理中最常用的是 2 型和 3 型。通常用 3 型文法或正则表达式描述词法结构,用 2 型文法描述表达式、语句等语法结构;“文法”无特殊说明时通常指上下文无关文法。
推导、句型、句子、语言
核心关系是:
S⇒∗α
表示从开始符号 S 经过零步或多步推导得到 α。
| 概念 | 定义 | 例子 |
|---|
| 推导 | 用产生式把某个非终结符替换成右部 | E⇒E+T |
| 句型 | 从 S 推导出来的任意符号串 | E+T、i+T、i+i |
| 句子 | 只含终结符的句型 | i+i |
| 语言 | 文法能推出的所有句子的集合 | L(G)={w∣S⇒∗w, w∈VT∗} |
句型可以含非终结符,句子只含终结符,语言是所有句子的集合。句子是句型的特例。
语法树
语法树是推导过程的图形表示。它的规则是:
- 根节点标记为开始符号 S
- 内部节点通常是非终结符
- 叶节点可以是终结符,也可以是非终结符
如果某个内部节点 A 有从左到右的孩子 X1,X2,…,Xn,那么文法中必须有产生式:A→X1X2⋯Xn。
语法树既反映推导过程,也反映句型的层次结构。语法树的“父节点扩展为子节点”对应一次产生式使用。
语法树的所有叶节点从左到右连起来,就是一个句型。
如果所有叶节点都是终结符,那么这个叶节点串就是句子。
子树和简单子树
- 子树是从某个节点开始,把它下面所有后代节点都包含进去形成的树。
- 简单子树,也叫直接子树,是某个节点和它的直接孩子组成的一层树。它直接对应一个产生式。
简单子树的特点是“一步展开”。普通子树可以包含多步展开。简单子树是最小的结构单元,直接对应文法中的一个产生式规则。
短语、简单短语、句柄
这三个概念都要放在某个具体句型和某棵语法树中理解。
短语
如果某棵子树的叶节点从左到右连起来是 β,那么 β 是该句型的一个短语。
形式化地说:
S⇒∗αAγA⇒∗β
所以:
S⇒∗αAγ⇒∗αβγ
那么 β 是句型 αβγ 相对于 A 的短语。短语对应子树,允许多步推导。
简单短语 / 直接短语
如果某棵简单子树的叶节点从左到右连起来是 β,那么 β 是简单短语。简单短语对应简单子树,只看一步产生式右部。
句柄
句柄是当前句型中需要被归约的简单短语。按照笔记里的表述,它是最左简单子树叶节点从左到右组成的符号串;在自底向上分析中,每一步识别并归约的对象就是句柄。对于无歧义文法,句柄唯一。
记忆方式:句柄是自底向上分析中“当前该收回去”的那一段。
包含关系
可以这样记:
简单短语 ⊆ 短语
句柄 ∈ 简单短语集合
也就是说,短语范围最大,简单短语更小,句柄是分析过程中当前要归约的那个简单短语。
更具体地说:
- 子树 ⟷ 短语
- 简单子树 ⟷ 简单短语
- 最左简单子树 ⟷ 句柄
- 整棵语法树的叶节点 ⟷ 句型或句子
三个集合的求法

First 集
找到左部是该非终结符的展开式,看得到的第一个项。如果得到的是终结符,就加入 First 集;否则继续递归查询;如果得到的是 ε,就也加进来,并计算下一个符号。First 可以有空串。
Follow 集
按下面的步骤来做:
- 输入符首先有 #。
- 看右部,找到该非终结符,如果它后面跟着终结符,则把这个终结符加入 Follow 集;否则加入后面这个非终结符的 First 集减去 ε。
- 看右部,找到该终结符,如果它后面的非终结符能够消失变成 ε,或者它后面没东西,那就加入左部的 Follow 集。
Follow 集不会有空串。
Predict 集
对于:
A→b…
就是求:
Predict(A→b…)={First(b)[First(b)−ε]∪Follow(A),ε∈/First(b),ε∈First(b)
例题

首先需要包含 T′ 的 First 集减去 ε。
T′ 的 First 集是 {ε,∗}。可以是空串,所以要加入左部的 Follow 集。
然后,加入 T 的 Follow 集。现在开始求 T 的 Follow 集。它应该首先包含 E′ 的 First 集,也就是 {ε,+},又因为 E′ 可以是空,所以要加入 E 和 E′ 的 Follow 集。
E 的 Follow 集显然是 {#,)}。Follow(E′)⊂Follow(E) ,所以综上所述,应该选择 C。
LL(1) 分析表
标准构表流程是:先求每个产生式右部的 FIRST,再求各非终结符的 FOLLOW,最后按规则把产生式填入表格。

终结符集合是:a,b,d,e,#
非终结符集合是:S,T,R,D
先求 FIRST 集。
D→a∣bd,所以:FIRST(D)=a,b
R→dR∣ε,所以:FIRST(R)=d,ε
T→DR∣ε,因为 FIRST(D)=a,b,所以:FIRST(DR)=a,b
再加上 T→ε:FIRST(T)=a,b,ε,S→eT∣RT
其中:FIRST(eT)=e
再看 RT:FIRST(R)=d,ε
因为 R 可以推出 ε,所以还要看 T:FIRST(T)=a,b,ε
因此:FIRST(RT)=d,a,b,ε
所以:FIRST(S)=e,d,a,b,ε
再求 FOLLOW 集。
开始符号是 S,所以:#∈FOLLOW(S)
因此:FOLLOW(S)={#}
看产生式:S→eT
T 在产生式末尾,所以:FOLLOW(S)⊆FOLLOW(T),得到:#∈FOLLOW(T)
再看:S→RT,R 后面跟着 T,所以:FIRST(T)−ε⊆FOLLOW(R)
因为:FIRST(T)=a,b,ε,所以:a,b⊆FOLLOW(R)
又因为 T⇒∗ε,所以:FOLLOW(S)⊆FOLLOW(R),得到:#∈FOLLOW(R)
同时 T 在末尾,所以:FOLLOW(S)⊆FOLLOW(T),因此:FOLLOW(T)={#}
再看:T→DR,D 后面跟着 R,所以:FIRST(R)−ε⊆FOLLOW(D),因为:FIRST(R)=d,ε,所以:d∈FOLLOW(D)
又因为 R⇒∗ε,所以:FOLLOW(T)⊆FOLLOW(D),得到:#∈FOLLOW(D)
因此最终为:
FOLLOW(S)={#}
FOLLOW(T)={#}
FOLLOW(R)=a,b,#
FOLLOW(D)=d,#
构造 LL(1) 分析表的规则是:
对于产生式 A→α:
如果 x∈FIRST(α),且 x=ε,则把 A→α 填入 M[A,x]。
如果 ε∈FIRST(α),则对所有 y∈FOLLOW(A),把 A→α 填入 M[A,y]。
逐条填表。
对 S→eT:
FIRST(eT)=e
所以填:
M[S,e]=S→eT
对 S→RT:
FIRST(RT)=d,a,b,ε
所以先填:
M[S,d]=S→RT
M[S,a]=S→RT
M[S,b]=S→RT
因为 ε∈FIRST(RT),还要看:
FOLLOW(S)={#}
所以填:
M[S,#]=S→RT
对 T→DR:
FIRST(DR)=a,b
所以填:
M[T,a]=T→DR
M[T,b]=T→DR
对 T→ε:
因为右部是 ε,看:
FOLLOW(T)={#}
所以填:
M[T,#]=T→ε
对 R→dR:
FIRST(dR)=d
所以填:
M[R,d]=R→dR
对 R→ε:
看:
FOLLOW(R)=a,b,#
所以填:
M[R,a]=R→ε
M[R,b]=R→ε
M[R,#]=R→ε
对 D→a:
M[D,a]=D→a
对 D→bd:
M[D,b]=D→bd
最终 LL(1) 分析表是:
| 非终结符 | a | b | d | e | # |
|---|
| S | S→RT | S→RT | S→RT | S→eT | S→RT |
| T | T→DR | T→DR | | | T→ε |
| R | R→ε | R→ε | R→dR | | R→ε |
| D | D→a | D→bd | | | |
空白格表示分析到对应“非终结符 + 当前输入符号”组合时出错。
自顶向下分析
在这门课、这类题的语境里,LL(1) 文法和不带回溯的基本可以当成一样:构造“不带回溯的自顶向下语法分析器”的判定条件,就是文法满足 LL(1) 的条件。标准判据是:对于同一非终结符 A 的任意两个不同候选式:
A→αi∣αj
要求:
PREDICT(A→αi)∩PREDICT(A→αj)=∅,i=j
展开成常见的两条条件就是:
- 候选式的开头集合互斥:
FIRST(αi)∩FIRST(αj)=∅,i=j
- 如果某个候选式能推出空串:
αi⇒∗ε
则它和其他候选式之间还要满足:
FIRST(αj)∩FOLLOW(A)=∅,i=j
原因是:当分析栈顶是 A,当前输入符号是 a 时,分析器需要根据 a 唯一选择一条 A 的产生式。PREDICT 集两两不相交,就能唯一选择;有交集,就会出现多个候选式都可选,需要回溯或报冲突。
更精确地说:
- “不带回溯”描述的是分析器行为:看当前输入符号即可决定规则。
- “LL(1)”描述的是文法性质:从左到右扫描输入,构造最左推导,只向前看 1 个符号即可决定规则。
判断 LL(1) 文法
快速判断 LL(1) 文法,先看三件事:
第一,看有没有左递归。有左递归基本直接排除 LL(1)。例如 S→Sab,这是直接左递归,因为右部一开始就是 S。所以 B 排除。
第二,看同一个非终结符的多个候选式,开头符号是否冲突。也就是判断 FIRST(αi)∩FIRST(αj)=∅,如果两个候选式都能以同一个终结符开头,就无法根据一个输入符号决定选哪条产生式。
第三,如果有 ε 产生式,再额外看 FIRST(αj)∩FOLLOW(A)=∅

这道题没有 ε,所以只看前两步就够了。
逐项看:
A:S→aSb∣ab
两个候选式都是以 a 开头:
FIRST(aSb)=a
FIRST(ab)=a
所以:
FIRST(aSb)∩FIRST(ab)=a
有冲突,排除。
B:S→ab∣Sab
这里有直接左递归:
S→Sab
直接排除。
C:S→aSb∣b
两个候选式开头分别是 a 和 b:
FIRST(aSb)=a
FIRST(b)=b
所以:
FIRST(aSb)∩FIRST(b)=∅
没有 ε 产生式,也没有左递归,所以它是 LL(1) 文法。
D:S→aS∣a
两个候选式都是以 a 开头:
FIRST(aS)=a
FIRST(a)=a
所以:
FIRST(aS)∩FIRST(a)=a
有冲突,排除。
最终选 C。
下列文法中属于LL(1)文法的是:
A. G[S]:
S → ABc
A → a | ε
B → b | ε
B. G[S]:
S → Ab
A → a | B | ε
B → b | ε
C. G[S]:
S → ABBA
A → a | ε
B → b | ε
D. G[S]:
S → aSe | B
B → bBe | C
C → cCe | d
正确答案AD,
判断 LL(1) 文法最稳的方法是:只比较同一个非终结符的不同候选式,看它们的 PREDICT 集是否两两不相交。
规则:
若 A→α,且 α 不能推出空串:
PREDICT(A→α)=FIRST(α)
若 A→α,且 α⇒∗ε:
PREDICT(A→α)=(FIRST(α)−ε)∪FOLLOW(A)
然后检查:
PREDICT(A→αi)∩PREDICT(A→αj)=∅,i=j
A 是 LL(1)。
文法:
S→ABc
A→a∣ε
B→b∣ε
只需要检查 A 和 B 的候选式。
因为 A 后面是 Bc,所以:
FOLLOW(A)=FIRST(Bc)=b,c
因此:
PREDICT(A→a)=a
PREDICT(A→ε)=FOLLOW(A)=b,c
两者不相交。
因为 B 后面是 c,所以:
FOLLOW(B)=c
因此:
PREDICT(B→b)=b
PREDICT(B→ε)=FOLLOW(B)=c
两者不相交。
所以 A 满足 LL(1)。
B 不是 LL(1)。
文法:
S→Ab
A→a∣B∣ε
B→b∣ε
先看 B:
PREDICT(B→b)=b
PREDICT(B→ε)=FOLLOW(B)
B 出现在 A→B 的末尾,所以:
FOLLOW(A)⊆FOLLOW(B)
又因为 S→Ab,所以:
b∈FOLLOW(A)
于是:
b∈FOLLOW(B)
所以:
PREDICT(B→ε) 中也含有 b
于是:
PREDICT(B→b)∩PREDICT(B→ε)=∅
冲突,B 排除。
也可以从 A 看出冲突:A→B 可以推出 b,A→ε 也会在后继符号 b 时使用,看到 b 时无法确定选 A→B 还是 A→ε。
C 不是 LL(1)。
文法:
S→ABBA
A→a∣ε
B→b∣ε
问题出在第一个 B。
在 S→ABBA 中,第一个 B 后面跟着 BA。
所以:
FOLLOW(B) 至少包含 FIRST(BA)−ε
因为:
FIRST(B)=b,ε
FIRST(A)=a,ε
所以:
FIRST(BA)=b,a,ε
因此:
b∈FOLLOW(B)
再看 B 的两个候选式:
PREDICT(B→b)=b
PREDICT(B→ε)=FOLLOW(B)
由于 b∈FOLLOW(B),所以:
b∈PREDICT(B→ε)
于是:
PREDICT(B→b)∩PREDICT(B→ε)=b
有冲突,C 排除。
直观理解:在 ABBA 里,第一个 B 后面还能跟另一个 B,而 B 自己又能推出 ε。看到输入符号 b 时,分析器无法判断当前这个 b 是属于第一个 B,还是第一个 B 取空、交给后面的第二个 B。
D 是 LL(1)。
文法:
S→aSe∣B
B→bBe∣C
C→cCe∣d
没有 ε 产生式,所以只看同一左部候选式的 FIRST 是否相交。
对 S:
FIRST(aSe)=a
FIRST(B)=FIRST(bBe∣C)=b,c,d
所以:
a∩b,c,d=∅
对 B:
FIRST(bBe)=b
FIRST(C)=c,d
所以:
b∩c,d=∅
对 C:
FIRST(cCe)=c
FIRST(d)=d
所以:
c∩d=∅
所有同左部候选式都能用一个向前看符号唯一选择,所以 D 是 LL(1)。
最终答案:A、D。
这题的关键不是“文法里有没有 ε”,而是“有 ε 时,ε 产生式的 FOLLOW 会不会和其他候选式的 FIRST 冲突”。A 没冲突,B 和 C 有冲突,D 没有 ε 且首符号完全分开。
语法分析大题
例题 1
设有文法 G:
S→SaA | bB
A→aB | c
B→Bb | d
(1)消除该文法的左递归;
(2)给出修改后文法每条规则的 Predict 集;
(3)试写出改造后的文法的递归下降语法分析程序。
关于 (1):
左递归指某个非终结符可以推出以自己开头的形式:
A⇒+Aα
其中最常见的是直接左递归:
A→Aα∣β
这里的规则是:
A→Aα∣β
消除后变成:
A→βA′(最后一定是会有一个 β 在前面的)
A′→αA′∣ε
含义是:原来左递归表示“先有一个 A,再不断往右追加 α”;改写后变成“先生成一个基础部分 β,再用 A′ 不断追加 α”。
对 S:
S→SaA∣bB
这里:
α=aA
β=bB
所以改写为:
S→bBS′
S′→aAS′∣ε
对 A:
A→aB∣c
右部没有以 A 开头的候选式,所以不用改。
对 B:
B→Bb∣d
这里:
α=b
β=d
所以改写为:
B→dB′
B′→bB′∣ε
最终消除左递归后的文法是:
S→bBS′
S′→aAS′∣ε
A→aB∣c
B→dB′
B′→bB′∣ε
快速做法:看到 X→Xα∣β,直接改成 X→βX′,X′→αX′∣ε。这题分别对 S 和 B 套模板即可。
标准答案:
(1)消除左递归后的文法为:
S→bBS’
S’→aAS’ | ε
A→aB | c
B→dB’
B’→bB’ | ε
(2) 消除左递归后每个产生式的Predict集合为
Predict(S→bBS’)={b}
Predict (S’→aAS’)={a}
Predict (S’→ε)=
(3)消除左递归后的文法对应的递归下降语法分析程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void S()
{ if(token==b) {Match(b); B(); S’();}
else Error();}
void S’()
{ if(token==a) {Match(a); A(); S’();}
else if(token==#) {skip;}
else Error();}
void A()
{ if(token==a) {Match(a); B();}
else if (token==c) {Match(c);}
else Error();}
void B()
{ if(token==d) {Match(d); B’();}
else Error();}
void B’()
{ if(token==b) {Match(b); B’();}
else if (token==a | #) {skip;}
else Error();}
|
层数和偏移量的计算
做法固定为三步:
- 先确定变量使用位置所在的局部化单位
- 再按“由内到外”查找同名变量
- 最后用该声明所在层的起始偏移和类型大小计算偏移量
使用性标识符查表时要从当前层开始,逐层向外查找;声明性标识符要登记名字、种类、类型、层数、偏移等属性。
一般规则:
- 进入一个新的局部化单位,也就是新的
{ ... } 或函数体,层数加深一层。题目约定“每个局部化单位的起始偏移为 0”,所以每一层自己的局部变量都从偏移 0 开始分配。 - 同一局部化单位内,声明变量时按顺序分配偏移。整型、字符型占 1 个单元,实型占 2 个单元。变量的偏移量就是它在本层局部区中的起始位置。
- 使用变量时,采用近处优先原则:当前层有同名变量就用当前层的;当前层没有,再向外层查找。笔记中的例子也说明,内层变量会遮蔽外层同名变量。
约定每个局部化单位的起始偏移为 0,整型、字符型占 1 个单元,实型占 2 个单元。在下面的程序中,第 5 行代码中变量 i 的层数和偏移量是_______.
1 2 3 4 5 6 7 8
| int i , j ; int main() { char i; i=’A’; { float i; i = 0; } i = ‘B’; }
|
A. 1,0 B. 1,1 C. 2,0 D. 2,1
选择 B,因为 C 语言只有 0 层和 1 层之分。
四元式
| 类别 | 四元式 | 高级语言含义 |
|---|
| 算术运算 | (ADDI,a,b,t)、(ADDF,a,b,t) | t=a+b |
| 算术运算 | (SUBI,a,b,t) | t=a−b |
| 算术运算 | (MULTI,a,b,t)、(MULTF,a,b,t) | t=a∗b |
| 算术运算 | (DIVF,a,b,t) | t=a/b |
| 类型转换 | (FLOAT,x,−,t)、(int-to-real,x,−,t) | t=(float)x |
| 赋值 | (ASSIG,x,−,y)、(:=,x,−,y) | y=x |
| 地址计算 | (AADD,A,t,t′) | t′=Addr(A)+t |
| 无条件跳转 | (JMP,−,−,L) | goto L; |
| 真跳转 | (JMP1,E,−,L) | if (E) goto L; |
| 假跳转 | (JMP0,E,−,L) | if (!E) goto L; |
| if 条件 | (THEN,E,−,−) | E 为假时跳过 then |
| else 分界 | (ELSE,−,−,−) | then 执行完后跳过 else |
| if 结束 | (ENDIF,−,−,−) | if 结束定位 |
| while 开始 | (WHILE,−,−,−) | 循环入口 |
| while 条件 | (DO,E,−,−) | E 为假时跳出循环 |
| while 结束 | (ENDWHILE,−,−,−) | 跳回循环开始 |
| 标号 | (LABEL,−,−,L) | L: |
| 子程序入口 | (ENTRY,Label,Size,Level) | 函数/过程入口 |
| 过程返回 | (ENDPROC,−,−,−) | 过程结束返回 |
| 函数返回 | (ENDFUNC,−,−,−) | 函数结束返回 |
四元式统一格式是:(op,ARG1,ARG2,RESULT)。
其中 op 是操作码,ARG1、ARG2 是操作分量,RESULT 是结果分量;这些分量可以是常量、变量名、临时变量,也可以是带层数、偏移、访问方式的抽象地址。笔记中明确用四元式表示中间代码,并给出 (op,ARG1,ARG2,RESULT) 这个格式。
表达式与赋值类四元式
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (+,a,b,t) | 计算 a+b,结果放入 t | t=a+b |
| (∗,a,b,t) | 计算 a∗b,结果放入 t | t=a∗b |
| (:=,t,−,x) | 把 t 赋给 x | x=t |
| (int-to-real,10,−,t1) | 把整数 10 转成实数,放入 t1 | t1=(real)10 |
| (FLOAT,x,−,t) | 把整数型 x 转成实型,放入 t | t=(float)x |
可以把:
sum:=first+count∗10
翻译成:
(int-to-real,10,−,t1)
(∗,count,t1,t2)
(+,first,t2,t3)
(:=,t3,−,sum)
意思是先做类型转换,再做乘法,再做加法,最后赋值。
也可以把整数运算和实数运算区分开,例如:
| 四元式形式 | 含义 | 高级语言写法 |
|---|
| (ADDI,a,b,t) | 整数加法 | t=a+b |
| (ADDF,a,b,t) | 实数加法 | t=a+b |
| (MULTI,a,b,t) | 整数乘法 | t=a∗b |
| (MULTF,a,b,t) | 实数乘法 | t=a∗b |
| (DIVF,a,b,t) | 实数除法 | t=a/b |
| (SUBI,a,b,t) | 整数减法 | t=a−b |
例如表达式:
X∗2+A∗(i+1)/(j+1)
给出的四元式包含:
(FLOAT,2,−,t1)
(MULTF,X,t1,t2)
(ADDI,i,1,t3)
(FLOAT,t3,−,t4)
(MULTF,A,t4,t5)
(ADDI,j,1,t6)
(FLOAT,t6,−,t7)
(DIVF,t5,t7,t8)
(ADDF,t2,t8,t9)
这里可以看出:整数表达式用 ADDI,实数表达式用 ADDF,MULTF,DIVF,类型不匹配时先生成 FLOAT 转换四元式。
数组下标变量与地址计算类四元式
数组下标变量的中间代码重点是计算元素地址。
数组:A[i1][i2]⋯[in]
地址公式是:
Addr(A[i1][i2]⋯[in])=Addr(A)+(i1−L1)S1+(i2−L2)S2+⋯+(in−Ln)Sn
其中:
α=size(T)
Di=Ui−Li+1
Si=Di+1Di+2⋯Dnα
用这个公式讲下标变量地址计算。
常见四元式如下:
| 四元式形式 | 含义 | 对应高级语言含义 |
|---|
| (SUBI,i,L1,t1) | 计算下标差 i−L1 | 数组第 1 维偏移起点 |
| (MULTI,t1,S1,t2) | 计算 (i−L1)S1 | 第 1 维贡献的地址偏移 |
| (AADD,A,t2,t3) | 地址加法,得到 Addr(A)+t2 | 得到数组元素或部分地址 |
| (AADD,t3,t5,t6) | 继续累加下一维偏移 | 得到多维数组元素地址 |
例如 A[i]可能生成:
$ (\text{SUBI},i,L_1,t_1)$
(MULTI,t1,α,t2)
(AADD,A,t2,t3)
最终 t3 表示 A[i] 的地址。
例如 A[i][j] 可能生成:
(SUBI,i,L1,t1)
(MULTI,t1,S1,t2)
(AADD,A,t2,t3)
(SUBI,j,L2,t4)
(MULTI,t4,S2,t5)
(AADD,t3,t5,t6)
最终 t6 表示 A[i][j] 的地址。
所以数组类题要抓住:SUBI 算下标差,MULTI 算偏移量,AADD 算地址。
无条件与条件跳转类四元式
转移性四元式会在目标代码中生成跳转指令。
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (JMP,−,−,L) | 无条件跳转到标号 L | goto L; |
| (JMP1,E,−,L) | 若 E=1,跳转到 L | if (E) goto L; |
| (JMP0,E,−,L) | 若 E=0,跳转到 L | if (!E) goto L; |
这三个是最通用的跳转四元式:
(JMP,−,−,L):直接跳。
(JMP1,E,−,L):条件为真跳。
(JMP0,E,−,L):条件为假跳。
if 语句相关四元式
if-else 的结构是:
(THEN,E.FORM,−,−)
S1 的中间代码
(ELSE,−,−,−)
S2 的中间代码
(ENDIF,−,−,−)
并且 THEN 和 ELSE 的跳转目标地址需要回填。
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (THEN,E,−,−) | if 条件控制;按笔记/题库口径,E 为假时跳到 else 或 endif | if (E) then S1 中的条件判断 |
| (ELSE,−,−,−) | then 子句结束后,跳过 else 子句 | else 分界 |
| (ENDIF,−,−,−) | if 语句结束定位 | if 语句结束 |
例子:
1 2 3 4
| if (a < b) x = 1; else x = 2;
|
可以抽象成:
(LT,a,b,t1)
(THEN,t1,−,−)
(ASSIG,1,−,x)
(ELSE,−,−,−)
(ASSIG,2,−,x)
(ENDIF,−,−,−)
回填后,THEN 跳到 else 分支,ELSE 跳到 endif 位置。
没有 else 时:
结构是:
(LT,a,b,t1)
(THEN,t1,−,−)
(ASSIG,1,−,x)
(ENDIF,−,−,−)
此时 THEN 的假出口回填到 ENDIF。
while 语句相关四元式
while 的中间代码结构是:
(WHILE,−,−,−)
E 的中间代码
(DO,E.FORM,−,−)
S 的中间代码
(ENDWHILE,−,−,−)
其中 WHILE 标记循环开始,DO 做条件转移,ENDWHILE 跳回循环头。
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (WHILE,−,−,−) | 定位循环开始入口 | while 的循环头 |
| (DO,E,−,−) | 条件转移;E 为假时跳出循环 | while (E) do 的条件判断 |
| (ENDWHILE,−,−,−) | 循环体结束,跳回 WHILE 处 | 循环末尾回跳 |
例子:
1 2 3
| while (a < b) { a = a + 1; }
|
可抽象成:
(WHILE,−,−,−)
(LT,a,b,t1)
(DO,t1,−,−)
(ADDI,a,1,t2)
(ASSIG,t2,−,a)
(ENDWHILE,−,−,−)
这里 DO 的假出口通常要回填到循环后继位置。
标号与 goto 类四元式
标号定位和 goto 语句主要用:
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (LABEL,−,−,L) | 定义标号位置 | L: statement |
| (JMP,−,−,L) | 无条件跳转到标号 L | goto L; |
LABEL 是定位性四元式,本身不产生跳转;JMP 是转移性四元式,会生成跳转指令。在基本块划分中也把 LABEL 归为标号性/定位性四元式,把 JMP 归为转移性四元式。
例子:
1 2 3 4
| goto L1; x = 1; L1: x = 2;
|
可抽象成:
(JMP,−,−,L1)
(ASSIG,1,−,x)
(LABEL,−,−,L1)
(ASSIG,2,−,x)
如果 goto L1 出现时还不知道 L1 的具体内部标号,就先生成缺目标地址的跳转四元式,后面遇到 L1: 时再回填。
过程、函数入口与返回类四元式
在基本块划分里列出:
| 四元式形式 | 含义 | 对应高级语言写法 |
|---|
| (ENTRY,Label,Size,Level) | 子程序入口,记录入口标号、活动记录大小、层数 | 函数/过程定义入口 |
| (ENDPROC,−,−,−) | 过程返回,跳转到过程调用后的下一句 | return; 或过程结束 |
| (ENDFUNC,−,−,−) | 函数返回,跳转到函数调用处后继位置 | return expr; 或函数结束 |
ENDPROC、ENDFUNC 归为转移性四元式,把 ENTRY 归为标号性四元式。
另外,运行时存储部分提到:遇到 call 四元式时申请新的活动记录,遇到 return 或函数结束时释放当前活动记录。也就是说,过程调用类四元式会触发活动记录申请和返回恢复。
基本块划分中特别提到的四元式类别
可以把四元式按基本块作用分成几类。
转移性四元式
这些四元式会导致当前基本块结束:
(JMP,−,−,L)
(JMP1,E,−,L)
(JMP0,E,−,L)
(ENDPROC,−,−,−)
(ENDFUNC,−,−,−)
(THEN,E,−,−)
(ELSE,−,−,−)
(DO,E,−,−)
(ENDWHILE,−,−,−)
标号性 / 定位性四元式
这些四元式通常作为新基本块入口:
(LABEL,−,−,L)
(ENTRY,Label,Size,Level)
(WHILE,−,−,−)
(ENDIF,−,−,−)