前面的文章中我们简单介绍了高级语言的五大语法元素,以及这些语法元素是如何构成表达式,构成语句最终构成可执行的代码的。我们也简单介绍了一些编程范式,可以写出'人能读懂的代码'了。
那么机器是怎么'读'我们的代码的呢?我们称机器'读'我们的代码的过程为代码的编译。机器'读懂'我们用高级语言编写的代码可以大致分为两个阶段:
分析我们的代码,明白代码的意图。
把高级语言代码映射成目标语言。
帮助机器读我们代码的也是一个程序,我们称之为编译器,和上面两个过程对应,编译器在解析我们的代码的时候也可以大致分为两个过程,分别为前端和后端,大致过程如下图所示:
对于某些解释运行的语言,可能并不会生成目标代码,而是在语义分析之后直接在解释器中组织程序的上下文运行程序,不过为了兼顾性能,采用这种解析方式的解释器也不是特别多。
从上面的图中我们可以看出,编译器的前端就是编译器对程序代码的分析和理解过程,它通常只和语言的语法有关,跟目标机器无关;而与之对应的后端则是生成目标代码的过程,这是跟目标机器有关的。
我们这篇文章主要要介绍的是机器是如何'读懂'我们的代码的,所以我们重点介绍的就是编译器的前端。
编译器前端的这三个过程其实和我们人类阅读文章的过程是非常相似的,凑巧的是,我最近正在恶补英文(出来混,总是要还的),而且还花重金购买了有道精品课的《逻辑英语》课程,那么我们就用阅读英文短文的过程来类比一下高级语言代码的编译前端,英文物料如下:
Lily is my first girlfriend,
she got married last year,
but the bridegroom is not me.
请相信我,这是个悲伤的故事。
词法分析(Lexical Analysis)
我们阅读英文文章,第一件事情就是认识单词。编译器识别我们的代码也是一样的,它首先要做的也是认识一个一个的‘单词’,只不过在编程语言的领域中,识别出来的不是单词,而称作词法记号,英文中就叫token。
举个例子,看看下面这段代码,如果编译器要读懂它,首先要怎么做呢?
#include
int main(int argc, char* argv[]){
int age = 45;
if (age >= 17+8+20) {
printf("Hello old man!\\n");
}
else{
printf("Hello young man!\\n");
}
return 0;
}
编译器会识别出 if、else、int 这样的关键字,main、printf、age 这样的标识符,+、-、= 这样的操作符号,还有花括号、圆括号、分号这样的符号,以及数字字面量、字符串字面量等。这些都是 Token。
这个过程其实和我们阅读英文文章的过程是非常类似的,单词是由一个个字母组成的,单词之间用空格或者是标点符号来分隔,我们利用空格或者标点符号把一串连在一起的字符识别成一个单词,就像上面英文物料中的第一行:
Lily is my first girlfriend,
我们会自然而然的根据空格把上面的文字分成Lily
、is
、my
、first
、girlfriend
这五个部分,这个过程就叫做'分词'。如果你要研发一款全文检索引擎,就需要有分词的功能。
正则文法和有限自动机
可以看出的是,在词法分析阶段,编译器处理源代码的粒度是基于字符的,经过词法分析之后,源代码的一个个字符就进化成为了一个个token。我们人类在阅读文章的时候,可以自然而然地根据空格和标点符号来进行'分词',但是编译器却是无法凭感觉做事的,它毕竟只是一个程序,只能按照规则办事。我们基于代码age >= 45
举几个例子:
识别 age 这样的标识符。它以字母开头,后面可以是字母或数字,直到遇到第一个既不是字母又不是数字的字符时结束。识别 >= 这样的操作符。
当扫描到一个 > 字符的时候,就要注意,它可能是一个 GT(Greater Than,大于)操作符。但由于 GE(Greater Equal,大于等于)也是以 > 开头的,所以再往下再看一位,如果是 =,那么这个 Token 就是 GE,否则就是 GT。
识别 45 这样的数字字面量。当扫描到一个数字字符的时候,就开始把它看做数字,直到遇到非数字的字符。
这些规则我们可以用词法分析器的生成工具来生成,比如 Lex(或其 GNU 版本,Flex)。这些生成工具是基于一些规则来工作的,这些规则用“正则文法”表达,符合正则文法的表达式称为“正则表达式”。生成工具可以读入正则表达式,生成一种叫“有限自动机”的算法,来完成具体的词法分析工作。
比如