编译原理
编译器
编译器是把源程序从一种表示变成另一种表示。
编译原理的四大部分:前端:词法分析、语法分析、语义分析;后端:代码生成。
编译器 具有非常 模块化的高层结构
词法分析
词法分析:检查字符流的单词合法性,生成记号流。【构成源程序的字符流,按照编程语言的词法规则将字符流组成词法记号流(token)】
字符流:和被编译的语言密切相关
记号流:编译器内部定义的数据结构,编码所识别出的词法单元。
词法分析器的实现方式:
- 手工编码实现法
- 先对复杂,容易出错
- 目前较为流行
- 词法分析器的生成器
- 可以快速原型,代码量较少
- 较难控制细节
有限状态自动机:
- 确定状态有限状态自动机DFA
- 对任意的字符,最多有一个状态可以转移
- 非确定的有限状态自动机NFA
- 对任意的字符,有多于一个状态可以转移
自动生成的过程:
正规表达式-(Thompson算法)–>NFA-(子集构造法)–>DFA-(Hopcroft 最小化算法)–>词法分析器代码
Thompson算法:
DFA的邻接矩阵表示法

| 状态|字符 | a | b |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 2 | 1 |
| 2 | 2 | 2 |
a(b|c)^*^ NFA

子集构造法:
q0={n0}
q0-a->{n1,n2,n3,n4,n6,n9}=q1
q1-b->{n4,n5,n8,n9,n3,n6}=q2
q1-c->{n6,n7,n8,n9,n3,n4}=q3
q2-b->{n4,n5,n8,n9,n3,n6}=q2
q2-c->{n6,n7,n8.n9,n3,n4}=q3
q3-b->{n4,n5,n8,n9,n3,n6}=q2
q3-c->{n6,n7,n8,n9,n3,n4}=q3
a(b|c)^*^ DFA

| 状态|字符 | a | b | c |
|---|---|---|---|
| q0 | q1 | \ | \ |
| q1 | \ | q2 | q3 |
| q2 | \ | q2 | q3 |
| q3 | \ | q2 | q3 |
Hopcroft 算法:
1 | //基于等价类的思想 |
Hopsroft 算法就是先根据非终结状态与非终结状态将所有的节点分为 N 和 A 两大类。 N 为非终结状态,A 为终结状态,之后再对每一组运用基于等价类实现的切割算法。
切分为 N 和 A, N 是 q0, A 是 {q1, q2, q3};在 A 中,就字符 b 的状态转移,每个节点最后得到的都还是 A 这个状态,无法对 q1, q2, q3 进行区分。同理,c 也不能这三个节点进行切分。所以就将这三个节点融合为一个新的节点 q4。
状态跳转表
| 状态\字符 | a | b | c |
|---|---|---|---|
| q0 | q4 | \ | \ |
| q4 | \ | q4 | q4 |
例题:
作业2.2:在下面的C函数钟,按序列出所有的记号,并给每个记号以合理的属性值
1 | long gcd(long p long q){ |
一共有33组记号
关键字:long 、 if 、 else 、 return
运算符:% 、 == 、 0
标识符:gcd 、p 、q
界限符:,、{、}、(、)
作业2.3:叙述下列正规式描述的语言
(a)
以0开头和结尾的由0和1组成的串
(b)
包含空串的由0和1组成的串
(c)
倒数第三个字符为0的由0和1组成的串
(d)$$0^10^10^10^$$
只含有三个1,由0和1组成的串
(e)
由偶数个0和偶数个1组成的所有0和1的串
(f)
不包含011子串的所有由0和1组成串
(g)$$(01|1)^0^$$
不包含001子串的所有由0和1组成的串
例题2.4:为下列语言写出正规定义
例题:C语言中无符号整数如何用正则表达式表示?
(1|2|……|9)(0|1|2|……|9)*|0
例题:C语言中的标识符:以字母或下划线开头,后面跟0个或多个字母、数字、下划线。如何用正则表达式表示。
(a|b|……|z|A|……|Z|_)(0|1|…|9|a|…..|z|A.…..|Z|_)
例题:将下图(a|b)*ab的NFA转换为DFA,并画出状态转移表

n0={0,1,2,4,7}=A
A–a–>{3,6,7,8,1,2,4}=B
A–b–>{5,6,7,1,2,4}=C
B–a–>{3,6,7,8,1,2,4}=B
B–b–>{5,6,7,9,1,2,4}=D
C–a–>{3,6,7,8,1,2,4}=B
C–b–>{5,6,7,1,2,4}=C
D–a–>{3,6,7,8,1,2,4}=B
D–b–>{5,6,7,1,2,4}=C

| 状态\字符 | a | b |
|---|---|---|
| A | B | C |
| B | B | D |
| C | B | C |
| D | B | C |
例题:将下图的DFA化为最简DFA

例题:将下图f(i|e)e的DFA化为最简DFA

这个算法的正则表达式是 fee 或者 fie,可以通过 fee 或 fie 到达终结状态。
N : {q0, q1, q2, q4}
A : {q3, q5}
在 N 中 q0 和 q1 在接受 e 的条件下最终得到的状态还是在 N 的内部。所以可以将其根据 e 拆分成 {q0, q1}, {q2, q4}, {q3, q5}
对于 q2 和 q4 都可以接受 e ,而且最终达到的状态一致,所以不能再进行切分。
q0 和 q1 ,在接受 e 的时候, q0 最终得到还是在 {q0, q1}这个状态的结合中, q1 却会落在 {q2, q4} 的状态中,所以可以将 q0 和 q1 分为 {q0}, {q1}。

语法分析
语法分析:检查记号流的语法合法性,生成抽象语法树。【检查词法分析输出的记号流是否符合规则】
语法分析:是否符合主谓宾语法
语法分析的数学理论:上下文无关文法(CFG)
编译器的常用的语法分析方法有
- 自顶向下分析
- 递归下降分析算法(预测分析算法)
- 每个非终结符构造一个分析函数
- 用前看符号指导产生式规则的选择
- LL分析算法
- 动作是 推导
- 递归下降分析算法(预测分析算法)
- 自底向上分析
- LR分析算法
- 动作是 归约
文法的概念:
- 组成语言的基本形式是 句子(句子是由 单词序列构成(单词是由 基本符号 组成))
- 文法是阐述语法的一个工具,语句是语法的实例
推导:从非终结符推导至终结符
推导分为:
- 最左推导
- 最右推导(规范推导)
graph TB
no1[<句子>]-->no2[<主语>]-->no5[<代词>]-->no8[<我>]
no1-->no3[<谓语>]-->no6[<动词>]-->no9[<喜欢>]
no1-->no4[<宾语>]-->no7[<名词>]-->no10[<音乐>]
归约:从终结符归约至非终结符
graph TB
n0[<我>]-->n3[<代词>]-->n6[<主语>]-->n9[<句子>]
n1[<喜欢>]-->n4[<动词>]-->n7[<谓语>]-->n9
n2[<音乐>]-->n5[<名词>]-->n8[<宾语>]-->n9
上下文无关文法:
上下文无关文法G是一个四元组:G=(T,N,P,S)
- 终结符集合T
- 非终结符积集合N
- 产生式规则P
- 每条规则形式:X -> β
1β2β3… βn- 其中X∈ N,β
i∈(T∪N)
- 其中X∈ N,β
- 每条规则形式:X -> β
- S是唯一的开始符号(非终结符)
- S∈N
上下文无关文法例子:
例子1
S->N,V,N N -> s | t | g | w V -> e | d1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- G=(N,T,P,S)
非终结符N={S,N,V}
终结符T={s,t,g,w,e,d}
开始符号:S
- 例子2
- ```上下文无关文法
E->num
| id
| E + E
| E * E
G=(N,T,P,S)
非终结符N={E}
终结符T={num,id,+,*}
开始符号:E
最左推导:
每次总是选择最左侧的符号进行替换
定义文法G1如下:
分析树:
推导可以表达成树状结构
特点:
- 树中的每个 内部节点代表 非终结符
- 每个叶子节点代表 终结符
- 每一步推导代表如何从双亲节点生成它的直接孩子节点
E->num | id | E+E | E*E- E->E+T ->T+T ->F+T ->3+T ->3+T*F ->3+F*F ->3+4*F ->3+4*51
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
36
37
38
39
40
41
42
43
44
45
46
推导3+4*5这个句子
- 最左推导1:
E->E+E
->3+E
->3+E*E
->3+4*E
->3+4*5

- 最左推导2:
E->E*E
->E+E*E
->3+E*E
->3+4*E
->3+4*5

### **二义性文法:**
- 给定文法G,如果存在句子S,它有两棵不同的分析树,那么称G是二义性文法
- 解决方案:文法重写
- ```文法重写
E->E+T | T
T->T*F | F
F->num | id
消除左递归:
一个文法式左递归的,如果它有非终结符A,对某个串ɑ,存在推导A==>+Aɑ.自顶而下的分析方法不能用于左递归文法,因此需要消除左递归。由A->Aɑ的产生式引起的左递归称为直接左递归。
方法
A->Aɑ|β消除左递归
- A->βA’
- A’->ɑA’| ɛ
例如文法
S->Aa|b
A->Sd| ɛ中S=>Aa=>Sda,不是直接左递归,需要化成直接左递归,
- S->Aa|b
- A->Aad|bd| ɛ
最后消除左递归:
- S->Aa|b
- A->bdA’| ɛA’
- A’->adA’| ɛ
提左因子:
提左因子是一种文法变换
- 方法
- A->αβ
1| αβ2提取左因子- A->αA’
- A’->β
1| β2
- A->αβ
FIRST集:
- 一个文法的符号串α的开始符号集合FIRST(α):从非终结符N开始推导出的句子开头的所有可能终结符的集合,ɛ属于FIRST(α)
- 例:
- E->TE’
- E’->+TE’|ɛ
- T->FT’
- T’->*FT’|ɛ
- F->(E)|id
- FIRST(E)=FIRST(TE’)=FIRST(T)=FIRST(F)={(,id}
- FIRST(E’)={+,ɛ}
- FIRST(T’)={*,ɛ}
FOLLOW集:
- 非终结符A的后继符号集合FOLLOW(A)是所有句型中可以直接出现在A后面的终结符的集合,如果A是句型中最右边的非终结符,那么$属于FOLLOW(A)。
- 如果有产生式A->αB或A->αBβ,且β=>*ɛ,那么FOLLOW(A)的一切元素都要加入FOLLOW(B)中。
- 例:
- E->TE’
- E’->+TE’|ɛ
- T->FT’
- T’->*FT’|ɛ
- F->(E)|id
- FOLLOW(E)=FOLLOW(E’)={),$}
- FOLLOW(T)=FOLLOW(T’)={+,),$}
- FOLLOW(F)={+,*,),$}
FIRST和FOOLLOW总结:
[推荐B站视频]: https://www.bilibili.com/video/BV1Cu411m7VX/?share_source=copy_web&vd_source=89bd71848ae8204999c9aa3e48cc5694 “【3编译原理如何求first集和follow集(更正版)】”
NULLABLE集合:
- 非终结符X属于集合NULLABLE,当且仅当:
- 基本情况:X->
- 归纳情况:X->Y1,Y2…Yn
- Y1,Y2…,Yn是n个非终结符,且都属于NULLABLE集
- 归纳情况:X->Y1,Y2…Yn
- 基本情况:X->
LL(1)分析算法:
从左(L)向右读入程序,最左(L)推导,采用一个(1)前看符号
满足条件:A->α|β
算法基本思想
- 表驱动的分析算法
0:S->N V N 1:N->s 2: |t 3: |g 4: |w 5:V->e 6: |d- LL(1)含冲突的分析表 | N\T | s | t | g | w | e | d | | :--: | ---- | ---- | ---- | ---- | ---- | ---- | | S | 0 | 0 | 0 | 0 | | | | N | 1 | 2 | 3 | 4,5 | | | | V | | | | | 5 | 6 | - 冲突的检测: - 对N的两条产生式规则N->β和N->γ,要求FIRST(β)∩FIRST(γ)=\{\}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- LL(1)分析表
| N\T | s | t | g | w | e | d |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| S | 0 | 0 | 0 | 0 | | |
| N | 1 | 2 | 3 | 4 | | |
| V | | | | | 5 | 6 |
- LL(1)分析表中的冲突
```含冲突表
0:S->N V N
1:N->s
2: |t
3: |g
4: |w
5: |w V
6:V->e
7: |d构造预测分析表(利用FIRST集合和FOLLOW集合)
对文法的每个产生式A->α
步骤1:对FIRST(α)的每个终结符α,把A->α加入M[A,α]
步骤2:若ɛ在FIRST(α)中,对FOLLOW(A)中每个终结符b(包含$),加入A->α加入M[A,b]
例:
1:E->TE’
2:E’->+TE’
3:E’->ɛ
4:T->FT’
5:T’->*FT’
6:T’->ɛ
7:F->(E)
8:F->id
- FIRST(E)=FIRST(TE’)=FIRST(T)=FIRST(F)={(,id}
- FIRST(E’)={+,ɛ}
- FIRST(T’)={*,ɛ}
- FOLLOW(E)=FOLLOW(E’)={),$}
- FOLLOW(T)=FOLLOW(T’)={+,),$}
- FOLLOW(F)={+,*,),$}
预测分析表
自底向上分析算法:
- LR分析算法(移进-归约算法)
- 目前应用最广泛的一类语法分析器自动生成器中采用的算法
- 自底向上分析的基本思想:最右推导的逆过程
- 引入点记符
- 两个重要步骤:
- 移进:一个记号到栈顶上
- 归约:可以把归约过程看成把输入串归约成文法的开始符号
- 文法
- S->aABe
- A->Abc|b
- B->d
- 将句子abbcde归约成S
- 归约序列:abbcde,aAbcde,aAde,aABe,S
- 文法
LL(1)文法
%E6%96%87%E6%B3%95%E5%88%A4%E6%96%AD.png)
LL(1)文法判断
%E6%96%87%E6%B3%95%E5%88%A4%E6%96%AD_1.png)
LR(0)分析表
%E5%88%86%E6%9E%90.png)
SLR算法:

LR(1)
.png)
例题
例题1:试着构造下列语言的文法
G={N,T,P,S}
N={S}
T={a,b}
P={S->ɛ|ab|aSb}
G={N,T,P,S}
N={S,X,Y}
T={a,b,c,#,d}
P={S->X|Y ,X->#|aXb,Y->#|cYd}
G={N,T,P,S}
N={S,Y,Z}
T={a,b,c}
P={S->ɛ|aS|Y,Y->ɛ|bY|Z,Z->ɛ|cZ}
G={N,T,P,S}
N={S,S1,S2,A,B,C,D}
T={a,b,c}
P={S->S
1|S2,S1->AB, A->aAb|ϵ,B->cB|ϵ, S2->CD, C->bBc|ϵ, D->aD|ϵ}任何不是以0开始的所有奇整数的集合
奇整数:
奇整数的最后一位数字必须是奇数,即 {1,3,5,7,9}{1, 3, 5, 7, 9}{1,3,5,7,9}。非以 0 开头:
非以 0 开头意味着数字串的第一个字符不能是 000。有效形式:
- 单位数奇数:{1,3,5,7,9}。
- 多位奇数:{d1d2…dk∣d1≠0,dk∈{1,3,5,7,9},d2,…,dk−1∈{0,1,…,9}}。
非终结符
- S: 起始符号,表示整个数字串。
- D: 表示第一个非零数字(开头数字)。
- U: 表示单位数奇数(单个位数的奇整数)
- M: 表示中间可以是任意数字的部分(包含 0 到 9)。
- O: 表示奇数结尾。
文法规则
S→DO∣DMO
U→O
D→1∣2∣3∣4∣5∣6∣7∣8∣9
M→0M∣1M∣2M∣3M∣4M∣5M∣6M∣7M∣8M∣9M∣ϵ
O→1∣3∣5∣7∣9
G={N,T,P,S}
N={S,C,M,U,O}
P={S→U∣DMO
U->O
D→1∣2∣3∣4∣5∣6∣7∣8∣9
M→0M∣1M∣2M∣3M∣4M∣5M∣6M∣7M∣8M∣9M∣ϵ
O→1∣3∣5∣7∣9}
例题2: 考虑文法
(a) 为句子 abab 构造两个不同的最左推导,以此说明该文法是二义的。
(b) 为 abab 构造对应的最右推导。
(c) 为 abab 构造对应的分析树。
(d) 这个文法产生的语言是什么?
(a) S->aSbS S->aSbS
->abS ->abSaSbS
->abaSbS ->abaSbS
->ababS ->ababS
->abab ->abab可知改文法是二义的
(b)S->aSbS S->aSbS
->aSb ->aSbaSbS
->abSaSb ->aSbaSb
->abSab ->aSbab
->abab ->abab(c)
(d)产生a,b数量相等的ab串。
例题3:文法
$$R\to R’|’R\ \ |\ \ RR\ \ |\ \ R\ |\ (R)\ |\ a\ |\ b$$
产生字母表{a,b}上所有不含 ε的正规式。注意,第一条竖线是正规式的符号“或”,而
不是文法产生式右部各选择之间的分隔符,另外“*”在这儿是一个普通的终结符。该文
法是二义的。
(a) 为该文法写一个等价的非二义的文法。运算优先级:星闭包 (∗) > 连接 (RR) > 或 (∣)。
(b) 按上面两个文法构造句子 ab|ba 的分析树。
(a)
S->E
E->T|E’
E’->E’T
T->FT’
T’->T|ɛ
F->(E)|F*|a|b(b)
学不动了,后面再学吧,救救孩子吧
例题4:选择题
- 下列关于LL(1)分析表的说法中正确的是(A)
- A.LL(1)分析表指示栈顶为非终结符时面临输入符号应该选择的产生式。
- B.LL(1)文法的LL(1)分析表可以有多重定义。
- C.一个产生式只能在LL(1)分析表中填写一次。
- D.LL(1)分析表也可用于自底向上语法分析。
例题5:
求下面文法每个非终结符号的FIRST集和FOLLOW集,并构造下面文法的LL(1)分析表。
- S:=aAd
- A:=BC
- B:=b|ɛ
- C:=c|ɛ
FIRST(S)={a} |FOLLOW(S)={$}
FIRRST(A)={b,c,ɛ} |FOLLOW(A)={d}
FIRRST(B)={b,ɛ} |FOLLOW(B)={c,d}
FIRRST(C)={c,ɛ} |FOLLOW(C)={d}
LL(1)分析表
a b c d $ S 1 A 2 2 2 B 3 3 4 C 5 6
语义分析
语义分析:检查抽象语法树的合法性
语义分析:类型检查、变量声明检查、参数类型检查、参数数量检查、函数是否声明、函数是否定义
语法制导翻译
精简词法分析实现实验
一、实验目的
1、 深入理解有限自动机及其应用
2、 编辑一个词法分析器,了解计算机识别源程序字符串的过程。
二、实验内容和要求
实验内容:处理c语言源程序,对源程序进行编译预处理(去除注释、无用的回车换行找到包含的文件等)之后,对整个源程序进行分解,分解成一个个单词,这些单词有且只有五类,分别是标识符、保留字、常数、运算符、界符。
实验要求:编写一个简单的词法分析程序,
输入:源程序,
输出:二元组<单词本身,种别码>。
程序流程:
1、 打开源程序,读取文件内容,直至遇到“$”文件结束符,读取结束。
2、 对读取的文件进行预处理,从头到尾进行扫描,去除//和/* ***/的内容,以及一些无用的、影响程序执行的符号如换行符、回车符、制表符等**。(但是千万注意不要在这个时候去除空格)。
3、 对源文件从头到尾进行扫描,扫描前判断当前字符是不是空格,如果是,扫描下一个字符,直至不是空格,然后判断是不是字母,若是则进行标识符和保留字的识别;若这个字符为数字,则进行数字的判断。否则,依次对这个字符可能的情况进行判断,若是将所有可能都走了一遍还是没有知道它是谁,则****认定为错误符号,输出该错误符号,然后结束。每次成功识别了一个单词后,单词都会存在token[ ]中。然后确定这个单词的种别码,最后进行下一个单词的识别。(简单起见,数字只是整数。)
4、 主控程序主要负责对每次识别的种别码syn进行判断,对于不同的单词种别做出不同的反应,如对于标识符则将其插入标识符表中。对于保留字则输出该保留字的种别码和助记符,等等。**直至遇到syn=0;**程序结束。
备注:需要将识别的源代码放入.\source.txt文件中。
1 |
|
LUM图
graph TB
A[开始] --> B[打印源代码]
B -->|调用printSource|C[打开文件]
C -->|失败|D[/输出错误信息并结束/]
C -->|成功|E[打印文件内容]
E -->I[读取文件内容]
I --> J[关闭文件]
J --> K[预处理源代码]
K -->|调用preprocess|L[遍历字符]
L --> M[返回预处理后的源代码]
M --> N[扫描源代码]
N -->|调用scanner|O[初始化变量]
O --> P[遍历字符]
P -->|忽略空白字符|Q[处理标识符/保留字]
Q -->|处理数字|R[处理运算符和界符]
R -->|遇到$符号|S[结束扫描]
S --> T[结束]
P -->|结束|S
R -->|遇到未知符号|U[/输出错误信息/]
U --> P
- Title: 编译原理
- Author: 敬请T期待
- Created at : 2024-11-21 15:02:31
- Updated at : 2025-02-20 20:02:42
- Link: https://kingwempity.github.io/2024/11/21/编译原理/
- License: This work is licensed under CC BY-NC-SA 4.0.


