学过编译原理的人都知道,编译原理这门课确实难学。我所看的关于编译原理的书籍有大家所熟悉的紫龙书以及《编译原理及实践》,作者都为美国人。对于没有上过这门课的非计算机专业的学生来说,自学起来,真的是很难。由于没有一个好的导师能引导你入门,那种刚开始看的生涩和繁杂确实是非同一般。但是,我忍住,坚持住了,并对其中的内容有了自己的学习体会。然而不管怎样,我始终相信,如果有一个好的导师引导你的入门,那么你学习这门课的时间肯定比自己推敲的时间少了很多。
学习这门课的过程中要记住理解词义本身的意思,比如什么事正则表达式,什么是文法,BNF又是什么,为什么要进行词法分析,语法分析,语义分析等等。这些是在看得过程中自己必须时时刻刻需要理会的,否则看过一遍之后,你还是不知道,一个完整的编译器中,正则表达式在编译器工作的时候是做何作用的,是进行词法单元解析还是语句解析等等的疑问就会出现在你脑里,而你如果一开始就对这些概念抓得很好,那么你看完编译原理,再结合一个简单的编译原理源代码,就能深入理解编译原理,进而巩固自己所过的东西。
我觉得看编译原理之前,可以知道一些背景知识。其中背景知识一就是第一个正式推广的高级语言,FORTRAN语言,它是由美国著名的计算机先驱人物约翰巴克斯(JohnWarnerBackus)开发出的,而正是因为他,计算机高级编程语言有了一个BNF(巴克斯范式)规范,这样,编程语言才能有规可寻。背景知识二就是我们熟悉的C语言其第一个版本的编译器是用什么语言写的,我们在学习编译原理的课程的时候,一些编译器的代码是可以用C或者Java写的,但是,在C没出现之前,用什么来编译C的呢?据查询,第一个版本的编译器是用汇编语言实现的。
了解了背景知识后,我们也出现了一个疑问,学习编译到底有什么作用?其实对这个问题,我也是有这个疑问,我学编译原理这门课,是因为我对计算机学科很感兴趣,所以基本上计算机专业的一些课程我都会去学好来。而至于作用,我总觉得最基本的是让你的编程有个底,知道了编程语言总有个规范,这个规范规定你不能越界,否则你将被报错。而具体作用有哪些,据查询如下:1.可以用语法分析来分析出一段用户上传是否含有代码;2.分析和分解用户输入的SQL语句,理解是否有害和是否有SQL注入;3.在业务软件中结算方面允许用户输入条件表达式和四则运算,允许用户自定义结算公式或条件;4.实现编辑器,如xml编辑器、html编辑器等等。
了解了一些背景知识之后,我们就可以进入正题了。首先要知道的是一个编译器的大体结构:文件输入——>词法分析——>语法分析——>语义分析——>中间代码——>代码优化——>目标代码。那么根据这个大体结构,我们知道主要学习的就是词法分析、语法分析、语义分析、中间代码以及目标代码生成。而至于代码优化,这个就是类似翻译一遍英语文章,翻译好之后要检查使得译文信达雅,而代码优化也是这个意思,所以这部分对于初学者就是个无底洞。下面对编译器几个重要部分做讲解:
1.词法分析:首先是词法分析的作用:我们知道,无论是计算机语言还是我们人类语言,如果没有一个规范,我们就无法做到语言的统一,从而也就难以理解对方的意思。于是,我们人类语言就有了所谓的句子,而句子的组成就由各个部分组成,如这个<句子> -> <主语><谓语>,那么我们在理解对方的意思的时候就会把某个句子拆成主语和谓语,从而判定对方的意思。而这个也同样适用于计算机语言,计算机语言也要求在词法分析阶段先进行一个一个词的解析,这里词的术语叫做词素,而词素对应的词法单元表示为<token-name,attribute-value>. 其中,token-name是语法分析步骤中使用的抽象符号,而第二个为指向符号表中关于这个词法单元的条目。于是,在词法分析阶段,一段代码就会被这分析成一个个词法单元,而维护这些词法单元的表就是编译原理所说的符号表。而在词法分析中,涉及到本文开头所说的正则表达式,可以这么理解,正则表达式就是几乎所有词素(如position = initial + rate * 60的position就是一个词素,其对应的词法单元就是<id,1>,这里说几乎,是因为有些词素不能完全由正则表达式表示)的相关表示,如下例子:
letter_→A|B|…|Z|a|b|…|z|-
digit→0|1|…|9
id→ letter_(letter_| digit)*
正则表达式扩展后如下:
letter_→[A-Za-z]
digit→[0-9]
id→ letter _(letter_| digit)*
那么这里表示的就是我们经常见到的一个变量的定义,如int first1;那么这个first1就符合上面的正则表达式。词法分析进行上面这个正则表达式的识别代码如下:
if(Character.isLetter(peak)){ //判断是否为字母
StringBuffer b = new StringBuffer();
do{
b.append(peak);
readch();
}while(Character.isLetter(peak));
String s = b.toString();
Word w = (Word)words.get(s); //从符号表中(哈希表)获取字符串为s的词法单元
if(w != null){ //判断是否已经在符号表中了
return w;
}else{
w = new Word(s, Tag.ID); //不在符号表中,添加进符号表中
words.put(s, w);
return w;
}
}
2.语法分析:首先是语法分析的作用:正如前面词法分析所说的,我们分析了一个一个句子之后,我们就要判断他是否符合我们的语言规范,比如我们的其中一个语言规范就是一个句子由主谓宾组成的,那么你就不能变成主宾谓,如果你变成主宾谓了,那么我通过查询相关语言规范,发现没有这样的规范,那么我就会报错。这就是语法分析的作用。而我们一直所说的语言规范到底是什么呢?在计算机中,这样的语法规范就是上面所说的巴克斯范式(BNF)。因为BNF规定了我们的计算机语言应该是怎样的一个语言规范。而和BNF相关的就是我们接下来要说的上下文无关文法,简称文法。在没讲文法前,我们就得先了解产生式,因为文法是产生式的一个集合,所以只有先了解产生式,才能顺利了解文法。产生式的一个例子:
stmt → if(expr) stmt else stmt。
其中→读作“可以具有如下的形式”。在这样一个产生式中,像关键字if和括号这样的词法元素被称为终结符号(terminal)。而像stmt、expr这样的变量被表示为终结符号序列,称为非终结符号(nonterminal)。
而文法就是如上面的众多产生式组成的集合。而通过这些文法,我们的编译器就可以确定一段代码里,哪些语句是正确的,哪些是错误的。
3.语义分析:语义分析的作用:分析由语法分析器给出的语法单位的语义,然后检查运算的合法性、取值范围等等作用,如position = initial + rate * 60这个句子,如果我们分析到rate为浮点数的时候,那么在语义分析阶段就得把整形为60的这个Num变成是浮点数的。
4.中间代码生成:我们知道,前面所说的一个结构似乎是具有先后顺序的,也就是说只有先运行完词法分析,才进行语法分析。但是在实际编码实现的时候,是一个整体运行的情况。如果把编译器分成编译器前端和编译器后端,那么词法分析、语法分析、语义分析和中间代码属于编译器前端,而优化代码、运行环境和目标代码等属于编译器后台。那么编译源代码文件从编译前端一个个字符读入,在编译器前端有个输出,这个输出为一个文件输出,然后编译器后台又把这个文件读入,从而得到目标代码文件。所以相关编译器前端部分,大体的过程就是边解析边翻译,然后最后统一生成中间代码。而这个翻译过程就是编译原理经常讲到的语法制导和语法制导翻译的内容。现在这里主要讲解中间代码生成:产生式:
if-else语句:S→ if (B) s_1 else s_2
其生成中间代码的java实现如下:
public void gen(int b,int a){
int lable1 = newlable(); //B为真的标记
int lable2 = newlable(); //B为假的标记
expr.jumping(0, lable2); //若B为假,则跳到else语句
emitlable(lable1); //否则,打印开头标记
stmt1.gen(lable1, a); //打印S1的语句代码
emit("goto L" + a); //跳出到if-else语句结束位置
emitlable(lable2); //else的入口地址
stmt2.gen(lable2,a); //打印S2的语句代码
}
我们知道,if-else中,若B的语句为真,则进入if语句执行,若B为假则跳到else语句执行,所以我们定义两个标记,B为真时,标记为lable1,为假时,标记为lable2,那么当执行jumping方法时,若B为假,则到else语句的入口地址处,即emitlable(lable2),然后打印s_2代码并跳转到if-else语句后的标记a,若B为真,则到if语句的入口地址处,即emitlable(lable1),也打印s_1代码并跳转到if-else语句后的标记a。例子如下:
例子:
if(x> 100){
x =x - 100;
}else{
x= 100 - x;
}
中间代码:
L4: iffalsex > 100 goto L10
L9: x =x – 100 gotoL8
L10: x =100 - x
L8:
具体中间代码的相关知识在紫龙书的第6章中会讲解的很详细。
5.目标代码生成:我们知道,在生成目标代码的时候,我们得根据不同的平台做出不同的操作。对编译器来说,各个平台的不同,编译器在生成目标代码的时候是不一样。所以在学习如何生成目标代码的时候,要先了解运行时刻的环境,也就是紫龙书的第7章。我开发过arm下的linux设备驱动。当你在x86下的完成linux下的C程序之后,如果程序在x86直接编译然后移植到arm板下的话,那么程序将会运行不起来,而只有在x86下进行交叉编译然后移植到arm板下才可以。这是因为经过交叉编译后的目标代码符合目标机环境,而在宿主机上的直接编译只能在宿主机生才可以用。
上面就是对编译原理的一些学习理解。因为自己学的也不是很深,所以如果有什么理解错误的,还请见谅和提出改正。ps:我始终觉得学某个东西,应该学了之后就做点东西来用用,下面是我根据编译原理做的一个android工具: