编译原理(第二章1--正则表达式)

目录

1. 词法扫描概念引入 

1.1 输入输出、数据结构和识别探究

1.1.1 输入输出

 1.1.2 数据结构

1.1.3 预读和回退

1.2 什么形式?

1.3 说明

1.4 小结

2.正则表达式

2.1 语言引入

2.1.1 语言

2.1.2 语言上的运算

2.1.3 小练习

2.2 正则表达式和正则集

2.2.1 概念引入

 2.2.3 小练习

2.2.4 自然语言到正则式

2.2.5 小练习

1. 词法扫描概念引入 

1.1 输入输出、数据结构和识别探究

1.1.1 输入输出

如第一章所述,词法扫描作为编译的第一个阶段,其输入肯定是源代码字符串,输出则是单词符号--Token。那什么是Token呢?这需要慢慢引入。

我们知道,常见的高级语言中我们可以把代码划分成如下的几个种类:

  • 关键字:如C中的int,while,for等
  • 标识符:如我们自己定义的变量名、函数名和数组名等
  • 常数:1234、2231等
  • 界符:,;:等
  • 运算符:-,+,*,/,++,--等
  • 空白符:空格、制表符等
  • 注释

而所谓的词法分析,就是要从输入的源代码串中提取出不同字符对应的种类,因此我们说的Token其实就是常见形式如下的记号:

(单词种类,值)

这里还要区分一下,有的Token是一符一值的(如关键字),在表示Token时,它们不用表示值,因为这个种类就只有它自己,而有的是一符多值的(如标识符,常量),这就需要表示值。

举个例子如下,我们给出该段代码对应的单词符号

 1.1.2 数据结构

在1.1.1中我们给出了什么是Token和其对应的表现形式,那末我们可以探究一下,其在计算机中是通过什么样的数据结构实现的呢?

其实很简单,我们先枚举出各个种类

 typedef enum{IF, THEN, ELSE, PLUS, MINUS, NUM, ID,…}TokenType;

然后定义相应的Token(种类,值)的结构体:

 typedef struct
	{
        TokenType tokenval;
        union
        {  
           char* stringval;
		   int numval; 
        } attribute;
	} TokenRecord;

1.1.3 预读和回退

在1.1.1和1.1.2中我们建立了对词法扫描器的初步认识。这里我们还要引入一个重要的东西,那就是词法扫描中的预读和回退。

试想这样一个情形:

int a = 0;
a ++;
if(a > 2)
{
    printf("%s","哲学和科学是怎么做到不冲突的?")
}

对于‘++’这个运算符是词法扫描器是如何区别的呢?换一个问法就是为什么它不会把++识别为两个相加符号+,而是自增符号?

所以可以看出,词法分析器识别Token不能仅仅依靠当前的字符,不然当词法分析器读到++中的第一个+就会把+单独识别出来,这是不正确的。因此,词法分析器识别Token需要预读下一个字符辅助判断。

但是这样就会引出另一个问题:对于上述代码中的 if(a > 2) ,当词法分析器分析到>时,它需要预读一个字符(考虑到>=Token的存在),这样它就会预读出2,但是不幸的此处代码中>表示的是大于符号,是一个单独Token,这就很尴尬。因为预读导致提前抓取出的字符不属于当前的Token而是属于下一个Token,因此我们需要将提前抓取出来的数据给人家放回去,这就是回退。为了实现预读和回退,我们引入了缓冲区,也就是提前将源代码中的部分数据(如一行)读到缓冲区中,这样预读和回退都从缓冲区中抓取和放回

1.2 什么形式?

通过前面介绍的词法扫描器的输入输出和其对应的数据结构,希望我们的脑海里已经可以构建出这样一幅图:

词法分析器不断吞掉字符(注意是字符,是一个字符一个字符地吞),然后生成对应的Token。

这里我们跳出局部的视野,从整体来思考一个问题:

在整个编译阶段,词法分析器是以怎样的形式提供Token的?这里有两种方式:

  1. 一遍扫描整个源代码文件,识别出对应Token保存起来(也就是先把整个的源代码程序识别成一个个的Token保存起来)
  2. 作为独立的子程序,以接口的形式供语法分析器使用(也就是语法分析器需要一个Token时就调用一次词法扫描器,生成一个token)

一般来说我们选择的是第2种,因为第2种方式结构简洁清晰,且资源开销小(不用预先保存生成的大量Token)

1.3 说明

这里值得一提的是:

  • 自己定义的标识符不能跟关键字重复。通过对词法扫描器的初步介绍,相信我们能够理解为什么会这样要求。
  • 关键字不会单独作为一个状态来识别,而是作为标识符来识别。这就要求我们在识别出标识符后,还要进一步查关键字表(其实可以理解为就是一个关键字数组)来判断它是不是关键字。

1.4 小结

在本小节介绍中,我们围绕着词法分析器的输入输出(Token),重要数据结构和预读和回退做了相关描述,但是我们的讨论都只是一个大概,也就是像学画老虎一样,我们在第1小节中已经勾勒出了老虎的轮廓,现在我们进一步地探究如何画出老虎的面貌躯体细节——词法分析器是如何识别不同的Token?

还记得我们在第一章里说的吗?我们识别Token需要借助正则表达式和有限状态机的帮助,这是本章的核心重点,在下面的小节中我们会介绍,希望能够掌握。

2.正则表达式

如前所述,我们要想真正掌握词法扫描就必须学会使用正则表达式和有限自动机(DFA)这两个工具,而本小节会介绍正则表达式

2.1 语言引入

要想了解正则表达式,我们先介绍语言和语言上的运算。

2.1.1 语言

字母表∑是一个有穷符号集合

符号:字母、数字、 标点符号、 …


字母表中每一个元素称为一个字符

字母表上的字(也叫字符串)是指由字母表中的字符所构成的一个有穷序列

不包含任何字符的序列称为空字,记为ε

用∑*表示∑上的所有字的全集,包含空字ε 。例如:  ∑ = {a, b},则 ∑* = {ε, a, b, aa, ab, ba, bb, aaa,...}

2.1.2 语言上的运算

∑*的子集U和V的并运算定义为 U ∪ V={ \alpha \in | \alpha \inU 或 \alpha \inV }

例如: U={aa, aaa} V={b, abb} 那么U ∪ V={aa, aaa, b, abb}


∑*的子集U和V的连接(积)定义为 UV={ \alpha \in| \alpha \inU & \alpha \inV }

例如: U = {0, 1} V = {a, b}  那么UV = {0a, 0b, 1a, 1b}


V自身的n次积记为    Vn=VV…V。规定V0={\varepsilon},令 V*=V0∪V1∪V2∪V3∪… ,称V*是V的闭包;

记 V+=VV* ,称V+是V的正闭包。

2.1.3 小练习

令L表示字母的集合{A,B,…,Z,a,b,…,z},令D表示数字的集合{0,1,…,9},下面是一些从L和D构造的新语言,试着写出语言集合:

  • L ∪ D

{A,B,…,Z,a,b,…,z,0,1,…,9}

  • LD

{A0,A1,A2,A3...,z0,z1...z9}任意字母开头和任意数字结尾的两位字符串

  • L^{4}

{AAAA,AAAB,AAAC,...}所有长度为4的字母串

  • L*

任意长度字母构成的字符串

  • L(L ∪ D)*

任意长度字母和字符构成的以字母开头的字符串

  • D+

任意长度不为0的数字构成的数字串

2.2 正则表达式和正则集

2.2.1 概念引入

通过前面的介绍,我们熟悉了一些常见的语言符号和其含义及运算,现在让我们正式了解正则表达式

正则表达式(Regular Expression, RE )是一种用来描述正则语言的更紧凑的数学表示方法。 一个字集合是正则集当且仅当它能用正则式表示。

一个正则表达式能表示的正则集称为它能描述的语言。

编程语言的词法基本上都可以用正则表达式描述


正则表达式等价是指正则表达式的正则集相同

通过上述定义我们可以了解正则表达式和正则集的关系,现在我们对这两个概念进一步定义如下:

正则式和正则集的递归定义: 对给定的字母表\Sigma

(1)\varepsilon 和\phi都是\Sigma上的正则式,它们所表示的正则集为{\varepsilon}和\phi;

(2) 任何a\in \Sigma ,a是\Sigma上的正则式,它所表示的正则集为{a} ;

(3) 假定e1和e2都是\Sigma上的正则式,它们所表示的正则集为L(e1)和L(e2),则

  • (e1|e2)为正则式,表示的正则集为L(e1)\cupL(e2)
  • (e1.e2)为正则式,表示的正则集为L(e1)L(e2)
  • (e1)*为正则式,表示的正则集为(L(e1))*

仅由有限次使用上述三步骤而定义的表达式才是\Sigma上的正则式,仅由这些正则式表示的字集才是\Sigma上的正则集。 

其实可以粗略地认为正则式是由属于\Sigma 上的字符由前面介绍的并交和闭包三个运算有限次组合得来的。而这些正则式所代表的字集合就是正则集。

还有正则表达式的运算优先集如下:

运算定律如下:

 2.2.3 小练习

这里的练习直接有答案,我们要做的是利用前面对正则集的递归定义去思考为什么答案是这个,这样才能加深自己的印象。

Tips:遇到闭包先写\varepsilon ,防止遗忘!!! 

2.2.4 自然语言到正则式

前面我们的练习都是基于正则式到自然语言,然而我们使用正则表达式恰恰是逆过来的:从自然语言到正则式。

首先,我们给出这种情况的一般思路:

(1)观察符号串特点

(2)分组讨论 给出表达式

(3)不同分组表达式连接在一起即可

  • ∑={ a,b,c}   写出只包含一个b的字符串的正则表达式。

        (1)字符串的特点是只含一个b

        (2)考虑b的位置,可能出现在字符串开头,中间,尾部

        (3)因此我们有:(a|c)^{*}b(a|c)^{*} 当前面的闭包取空时b在开头,后面的闭包取空时b在尾部,两个都非空时,在中间

  • ∑={ a,b,c} 写出最多包含一个b的串的正则表达式.

        (1)字符串特点最多只含一个b的串

        (2)考虑b的个数,b个数可能取1-----(a|c)^{*}b(a|c)^{*},也可能取0----- (a|c)^{*}

        (3)因此我们有:(a|c)^{*}|(a|c)^{*}b(a|c)^{*}

  • ∑={ a,b,c} 写出一个任何两个b都不相邻的字符串的正则表达式

        (1)字符串特点任何两个b都不相邻(aba,ab,bab,baaaab.....)

        (2)考虑到b不相邻,那先构造出(ba|a)^{*},这样无论如何b都不会相邻,其次该字符串显然没有把题目要求的字符串全部表示完,因为其不能以b结尾,故有(ba|a)^{*}b

        (3)连接起来我们有:(ba|a)^{*}|(ba|a)^{*}b \Rightarrow (ba|a)^{*}(b|\varepsilon )

2.2.5 小练习

1.以01结尾的0、1串

2.能被5整除的十进制无符号整数串

3.不含子串abb 的由a、b组成的字符串

4.含有子串010的所有0、1串

5.每个1后面都有一个0的0、1串


答案如下:

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值