编译原理总结
1.编译程序概述
1.1概叙
计算机使用的语言可以分为:高级语言、汇编语言和机器语言三个层次。
编译程序可以将高级语言程序变换为可以在计算机上面执行的形式,同一种高级语言可以配置多种不同的编译程序。
1.2编译程序
1.2.1编译程序的功能
编译程序是专门以高级程序设计语言的源程序作为翻译对象进行翻译处理的,其基本功能是把高级语言源程序翻译成等价的目标程序,还具有语法检查、语义检查和错误处理等功能。
1.2.2解释程序
解释程序同样是将高级语言源程序翻译成目标语言程序,但是解释程序不是像编译程序那样将高级语言程序源程序翻译之后产生一个目标程序,而是边翻译边执行,直至整个程序执行完毕。
两者区别:编译程序先全部把源程序翻译成目标程序,然后再执行,而且目标程序可以反复执行。解释程序以源程序作为输入,但是不生产整个的目标文件,而是边解释边执行源程序本身。
1.2.3编译程序的工作过程
整体流程如下图所示
1.词法分析 :
从左到右一个字符一个字符地读入源程序,对构成源程序的字符流进行组合,构成单词,该阶段也被称为扫描源程序。该阶段的主要功能是审查源程序是否有此法错误,为代码生成阶段收集信息。
2.语法分析 :
在词法分析基础上,根据语言的语法规则对源程序的单词流进行分析,将单词符号串转化为语法单位(表达式 语句 程序 程序段),并确定整个输入串是否构成语法上正确的程序。
3.语义分析 :
对语法分析所识别出的各种语法单位,分析其含义,并初步翻译并产生中间代码。该阶段的主要功能是审查源程序有无语义错误,为代码生成阶段收集信息。
4.中间代码生成(非必需阶段):
编译程序将源程序变成一个内部表示形式。
5.代码优化 (非必需阶段):
对中间代码进行优化,提高目标程序的时间和空间质量。
6.目标代码生成:
将中间代码或经过优化处理的中间代码变换成低级语言代码。目标代码的形式可以是绝对及其指令代码、可重定位的机器指令代码或者汇编指令代码。
1.2.4编译程序的逻辑结构
通常情况下会把编译过程分为前端和后端,划分的依据是看编译程序是否与硬件有关。
前端依赖源语言而与目标机器的硬件无关,后端指那些依赖目标机器而一般不依赖源语言的部分。
符号表管理:符号表管理在编译程序中具有十分重要的意义,是不可或缺的一部分,用来登记源程序中出现的各类标识符及其语义属性。(编译程序大部分时间都化在符号表管理上)
出错处理:对源程序中的错误进行处理,尽可能让源程序运行下去并且能给用户一份详细的出错清单,方便用户调试程序。
1.2.5遍的概念
遍就是对源程序或源程序的中间结果从头到尾扫描一次,并做有关的加工处理,生成新的中间结果或目标程序。
多遍扫描特点:编译程序结构清晰,目标代码质量较高,但是编译速度较慢。
一遍扫描特点:编译程序结构复杂,需要占用较大空间,能提高编译速度。
2.高级语言的语法描述
2.1文法和语言的定义形式
2.1.1文法
文法简单来说是描述语言的语法结构的形式规则,即语法规则。
文法G[S]可用一个四元组定义,即G=(VN,VT,P,S)。其中VN是一个非终结符集合,VT是一个终结符集合,P是一组产生式集合,S∈VN,是文法开始符号。
文法分类:
2.1.2语言
由某个文法G[S]产生的所有句子的全体是一个语言集合,用L(G[S])表示。
2.1.3由文法推导句子
推导分为最左推导和最右推导。
最左推导:推导过程中每次只替换最左边的非终结符,直到推导出该句子。
最右推导:推导过程中每次只替换最右边的非终结符,直到推导出该句子。
2.2二义性
如果有一文法,该文法中存在某个句子对应两棵不同的语法树或者两种不同的最左推导或两种不同的最右推导,则称该文法为二义性文法。
如果要求证明某文法具有二义性,只需写出某个句子有两种不同的最左推导或者最右推导或者两棵不同的语法树即可。
2.3常见题型
- 例1(已知文法推导语言) 设有文法G[S]:S→bA,A→aA|a,求该文法产生的语言是什么?
答:
S=>bA=>ba
S=>bA=>baA=>baa
...
A=>bA=>baA=>...=>baaaa...a
所以该文法的句子为以b开头,后面为一个或多个a,即L(G1)={ba^n|n>=1}
- 例2(根据语言设计文法) 已知语言 L(G)={a^n c^m d^m b^n|n>=1,m>=0},请构造能产生该语言的文法。
答:
G[S]:
S→aSb|aTb
T→cTd|ε
- 例3(证明文法的二义性) 设有文法G[S]:S→aSb|Sb|b,证明该文法是二义的
答:
因为句子aabbbb存在两棵不同语法树,故该文法是二义的。
- 例4(根据特定描述要求设计文法) 设计一个文法,使其语言是偶数集合,且每个偶数不以0开头
答:
G[S]:
S→ABC
A→(1|2|3|4|5|6|7|8|9)
B→(0|1|2|3|4|5|6|7|8|9)*
C→(0|2|4|6|8)
3.词法分析
3.1词法分析器(扫描器)
词法分析器的主要任务是读入(源程序)字符串并识别出一个个具有独立含义的最小语法单位即单词。
词法分析器的输出结果是单词的种别编码和单词属性值。
单词的分类:
- 保留字(例如c语言中的main int bool等)
- 标识符(变量名、数组名、函数名等)
- 运算符(+、-、*、/等)
- 分界符(括号,逗号等)
- 系统函数名
词法分析器的组成:
扫描器、扫描缓冲区和预处理子程序。
3.2有限自动机
有限自动机是单词符号的自动识别机。
有限自动机的两种表示方法:
- 状态转化图(初态用=>标出,双圈的表示终态)
- 状态转化矩阵
有限自动机分为非确定的有限自动机(NFA)和确定的有限自动机(DFA)两种。
NFA和DFA的异同:
- NFA可以有若干个开始状态,而DFA仅只有一个开始状态。
- NFA后继状态不一定唯一,DFA后继状态唯一。
- NFA和DFA都可以有若干个终态。
3.3由正规式到DFA
正规式和正规文法是等价的,从正规式或正规文法可以构造出等价的有限自动机。
一般步骤:
- 由正规式构造NFA
- NFA确定化为DFA
- DFA的化简
例题:
- 例1
构造与正规式a*(b|a)(a|b)*ba等价的NFA,然后对NFA确定化为DFA,最后对DFA化简。要求NFA和DFA以图形的方式描述。
解:
由正规式确定的NFA:
确定化为DFA:
a | b | |
0 | 0,1 | 1 |
0,1 | 0,1 | 1,2 |
1 | 1 | 1,2 |
1,2 | 1,3 | 1,2 |
1,3 | 1 | 1,2 |
重命名后为:
a | b | |
A | B | C |
B | B | D |
C | C | D |
D | E | D |
E | C | D |
未化简的DFA状态图为:
化简DFA:
将所有状态分为终态和非终态两组 {A,B,C,D} {E}
第一次划分 {A,B,C} {D} {E}
第二次划分 {A} {B,C} {D} {E}
将所有的C状态替换为B,并删除C状态那一行,上表可更新为:
a | b | |
A | B | B |
B | B | D |
D | E | D |
E | B | D |
化简后的DFA状态图为:
4.语法分析
4.1语法分析程序
语法分析的任务是在词法分析识别出单词符号串的基础上,分析并判定程序的语句和程序结构是否符合语法规则。
语法分析程序的输入的是词法分析器生成的单词符号,输出的是语法分析树。
按照语法分析树的建立方法,把语法分析方法分为两大类:
- 自上而下的分析方法(递归下降分析程序构造和LL(1)分析方法)
- 自下而上的分析方法(算符优先分析方法和LR类分析方法)
4.2自上而下分析
自上而下的主旨是:对任何输入串,试图用一切可能的方法,从文法开始符号出发,反复使用文法的规则进行推导,自上而下地为输入串建立一颗语法树;或者说,为输入串找到一个最左推导。
4.2.1 自上而下分析的基本问题:
- 左递归--->死循环
- 回溯--->重复匹配
消除左递归:
设有文法规则:P->Pα|β
关于非终结符P的产生式右部第一个符号就是非终结符P,则称P->Pα为直接左递归。
可以通过引入新的非终结符P'来消除左递归,可以把P改写为以下非左递归形式:
P->βP'
P'->αP'|ε
例:
将 E的直接左递归产生式 E->E+b|a 消除左递归
解:
E->aE'
E'->+bE'|ε
消除回溯:
提取相交候选式的公共左因子。
例:
有文法G[U]:
U->aVc|aVb
提取公因子后:
U->aV(c|b)
改写文法得:
U->aVB
B->c|b
4.2.2 FIRST集:
令G是一个不含左递归的文法,对G的所有非终结符的每个候选α,定义他的终结首符集FIRST(α)为
FIRST(α)={a|α *=>a.....,a € VT},若α *=>ε,规定ε € FIRST(α)
说白了,FIRST(α)就是α的所有可能推导的开头终结符或者ε。
如果非终结符A的所有候选首符集两两不相交,那么对A的任意两个不同候选αi和αj,都有:
FIRST(αi)∩FIRST(αj)=Ø
构造方法:
例:
设有文法G[U]如下:
U->aVc|bVc
V->cc|dd
求非终结符的FIRST集
解:
FIRST(U)={a,b}
FIRST(V)={c,d}
4.2.3 FOLLOW集:
假设有文法G[S],对于文法G的任意非终结符B的FOLLOW(B)定义如下:
FOLLOW(B)={a|S *=>....Ba....,a € VT},若S +=>....B,规定# € FOLLOW(B)
简单来说,FOLLOW(B)是所有句型中出现在紧接B之后的终结符或"#"。
注意:FOLLOW集中不能有ε。构造方法:
例:
已知文法G[S]如下,求每个非终结符的FIRST集和FOLLOW集
S->ABa; A->b|eB|ε; B->d|ε
解:
FIRST(S)={b,e,d,a}
FIRST(A)={b,e,ε}
FIRST(B)={d,ε}
FOLLOW(S)={#}
FOLLOW(A)={d,a}
FOLLOW(B)={d,a}
4.2.4 LL(1)分析法
预测分析模型主要包括:输入串、栈、总控程序、预测分析表和输出。其中,总控程序和预测分析表是最为关键的两部分。
LL(1)的第一个L表示从左到右扫描输入串,第二个L表示最左推导,1表示每次只向前展望一个符号。
预测分析表的构造:
- 构造FIRST集
- 构造FOLLOW集
- 构造LL(1)分析表
FIRST和FOLLOW集的构造前面已经详细介绍过,不再过多介绍,下面重点介绍LL(1)分析表的构造算法。
对于某文法G的产生式:A->α1 |α2 |α3 |......|αn有:
- 对每个终结符a∈FIRST(α),将A->α’加到M[A,a]中。
- 如果ε∈FIRST(α),则对于任何b∈FOLLOW(A),将A->α’加到M[A,b]中。
- 无定义M[A,a]处为ERROR(一般在表中空白不写即可)。
注意:分析表中所有非终结符占一行,所有终结符(包括#)占一列
例:
设某去除左递归后的文法G[E]如下:
E->TE'
E'->+TE'|ε
T->FT'
T'->*FT'|ε
F->(E)|i
构造该文法发预测分析表。
解:
文法中每个非终结符的FIRST集和FOLLOW集分别为:
FIRST(E)={ ( , i } FOLLOW(E)={ #,) }
FIRST(E')={ + , ε} FOLLOW(E')={ #,) }
FIRST(T)={ ( , i } FOLLOW(T)={ +,#,) }
FIRST(T')={ *, ε } FOLLOW(T')={ +,#,) }
FIRST(F)={ ( , i } FOLLOW(F)={ *,+,#,) }
构造LL(1)分析表:
+ | * | ( | ) | i | # | |
E | E->TE' | E->TE' | ||||
E' | E'->+TE' | E'->ε | E'->ε | |||
T | T->FT' | T->FT' | ||||
T' | T'->ε | T'->*FT' | T'->ε | T'->ε | ||
F | F->(E) | F->I |
总结起来一句话:如果终结符a出现在非终结符A的FIRST集中,就在该行该列相交的格子里写上导致a在FIRST(A)里的产生式,另外如果FIRST(A)中还包含ε,把A->ε写在(A,α)位置,α是FOLLOW(A)中的任意非终结符。
4.3LL(1)文法满足的条件
设B->α1|α2|α3|....|αn。
- 文法不含左递归
- 对于文法中每一个非终结符B的每个候选式的首符集两两相交为空集,即FIRST(αi)∩FIRST(αj)=Ø,i≠j
- 对于文法中的每个非终结符B,若它的某个候选式首符集包含空ε,则B的FIRST集和FOLLOW集不相交
4.4自下而上分析
4.4.1简述
设G[S]是一个文法,S是文法的开始符号。
短语:语法树中每个以非终结符为根的树的所有叶子构成的串
直接短语:只有父子两代的子树的叶子组成的串
句柄:一个句型的最左直接短语
素短语:至少含有一个终结符,且除它自身外不再包含任何更小的素短语
最左素短语:一个句型中最左边的素短语
解:
短语:S(左)、S,a、a、(S)、S(右)、S,a,(S)、(S,a,(S))
直接短语:S(左)、a、S(右)
句柄:S(左)
素短语:a、(S)
最左素短语:a
4.4.2LR分析法
L表示从左到右扫描输入串,R表示最右推导(最左规约)。
LR类分析表一共有四种构造方法:
- LR(0)表构造法,最简单
- 简单LR表构造法(SLR表构造法)
- 规范LR表构造法,能力最强
- 超前LR表构造发(LALR)
4.4.3LR(0)项目集族和LR(0)分析表的构造
文法G的每一个产生式的右部添加一个黑色的小圆点成为一个LR(0)项目。
设有文法G[S]的拓展文法G[S']。
LR(0)项目可分为一下几类:
- A->α· 规约项目
- S'->S· 接受项目
- A->α·Xβ X属于VN,待约项目
- A->α·aβ a属于VT,移进项目
文法G[S]拓展为文法G[S'],引入非终结符S',增加S'->S的目的是使项目S'->S·作为唯一的接受项目。
1.定义和构造I的闭包
如果I是拓广文法G‘的一个项目集,定义和构造I的闭包CLOSURE(I):
1.I的项目均在CLOSURE(I)中;
2.若A->α · Bβ属于CLOSURE(I),则每一形如B-> · r的项目也属于CLOSURE(I)。
3.重复第二步直到不出现新的项目为止,闭包不再扩大。
例:
已知文法G[S']如下,设I={S'->S},构造其CLOSURE(I)。
(0)S'->S
(1)S->S(S)
(2)S->a
解:
CLOSURE(I)为:S'->·S,S->·S(S),S->·a
2.构造状态转换函数GO(I,X)
GO(I, X) = CLOSURE(J),其中I为包含某一项目集的状态,X为一文法符号,J={A->αX · β | A->α · Xβ属于I}。
即找到所有圆点· 后面是X的项目A-> A->α · Xβ,再将圆点向后移动一位,再令J={A->αX · β },最后求CLOSURE(J)。
3.构造识别G[S]所有活前缀的DFA算法
- 拓广文法G[S],增加产生式S'->S
- 令初态I0=CLOSURE(S'->·S)
- 对状态集C(C里存放的是所有的状态Ii)中当前的每一个状态Ii和对每一个文法符号X,计算K=GO(Ii,X),并对Ii到K画弧线,标记为X,若K不在C中,则K加入C中
- 重复第三步,直到C中不再增加为止
只说这些概念性的东西不是很好理解,下面用一道例题来把这三步详细介绍一下:
已知文法G[S']如下,构造其CLOSURE(I),构造其识别该文法活前缀的DFA。
(0)S'->S
(1)S->S(S)
(2)S->a
分析:
首先先令I0=CLOSURE(S'->·S),因为黑点后面是非终结符S,所以要把S的所有产生式右部第一个位置都加上黑点后加入到该状态中,
即I0={S'->·S,S->·S(S),S->·a}
接着对I0中的每一个文法符号X,求其GO(I0,X),如果求出的新状态未出现过,就加入状态集C中。
首先我们先找I0中的S,要找所有I0中所有黑点后面紧跟S的项目,可以找到{S'->·S,S->·S(S)},然后根据GO(I,X)的规则求CLOSURE({S'->S·,S->S·(S)}),求出的结果为
{S'->S·,S->S·(S)},我们可以发现是还未出现的新状态,将其加入C中,并记为I1={S'->S·,S->S·(S)}。
然后再找I0中的(,发现I0中没有黑点后面紧跟(的项目,然后继续找)发现也找不到,然后再找a的可以找到{S->·a},然后求CLOSURE({S->a·}),结果为{S->a·},又是一个新状态,令I2={S->a·}。
这样I0中所有文法符号就找完了,接着再从状态集C中新增的状态I1,I2中以同样方法寻找,直到找完C中不能再增加新状态为止。
结果:
I0={S'->·S,S->·S(S),S->·a}
I1={S'->S·,S->S·(S)} (通过GO(I0,S)得到)
I2={S->a·} (通过GO(I0,a)得到)
I3={S->S(·S),S->·S(S),S->·a} (通过GO(I1,S)得到)
I4={S->S(S·),S->S·(S)} (通过GO(I3,S)得到)
I5={S->S(S)·} (通过GO(I4,) )得到)
最后就是根据这个关系画DFA了,即如果通过G0(Ii,X)=Ij,就画一条线从Ii指向Ij,线上面写上X。
就比如I1=GO(I0,S),就有一条从I0通过S指向I1的线,I2=GO(I0,a),就有一条从I0通过a指向I2的线,以此类推,画出所有状态的关系指向即可。