前言
欢迎持续关注专栏:juejin.cn/column/7265…
上一篇文章我们简述了编译的4个步骤,这节我们来看看其中第二个流程即编译阶段,编译器都帮我们做了什么。
正文
从最原始的角度来看,编译器就是将高级语言编译成机器能够运行的语言的一种工具,注意,由于后面还有一个汇编器进行汇编操作的过程,所以这里的编译流程其实是编译成特定平台的汇编代码。
这里补充一个小知识点,为什么要有汇编代码?直接编译成二进制给机器执行不可以吗?
首先汇编代码是和操作系统和处理器强相关的,不同的处理器架构有着不同的指令集和执行方式,所以不同的处理器需要使用相应的汇编指令进行编程,比如x86架构和ARM架构都有各自的指令集和汇编语法。
操作系统管理与硬件交互的底层任务,并且提供高级抽象的接口供应用层程序使用,不同的操作系统可能有不同的系统调用、内存管理机制、进程间通信等功能,这些功能在汇编代码中会有不同的实现方式和调用约定。
因此编写汇编代码需要考虑所针对的处理器架构和操作系统,以保证代码能够与特定的硬件和操作系统进行交互,汇编代码在不同的处理器架构和操作系统之间不能通用,需要进行适配和修改。
所以我们就可以知道编译器把代码编译成汇编语言就已经足够了,而汇编器是把汇编语言编译成二进制而已。同时从另一个角度来看,汇编语言其实就是给人看的机器码,方便开发人员调试问题。
那为什么要使用高级语言来进行编程呢,原因最直观的有以下几点:
- 使用机器指令或者汇编语言写程序非常费劲,效率低下,不易阅读。
- 机器语言或者汇编语言一般都是依赖于特定的机器,程序员可不想为每一种CPU架构都写一份代码。
- 使用类似自然语言的高级语言,比如C/C++等,可以更好地组织代码,使用各种设计模式,再通过编译器编译成不同平台的机器码,可以提高效率。
- 高级语言可以让程序员更关注逻辑,而不是计算机本身的限制,比如字长、内存大小等。
所以这个编译过程非常复杂,大致可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化,整个过程如图:
能够理解这6大过程,可以很大程度提高对编程语言的理解,尤其是日常工作中碰到的语法错误、优化等现象,甚至理解编译过程,我们可以自己创造一门语言。
词法分析
首先是将源代码程序输入到扫描器中,进行词法分析,把源代码中的字符序列分割成一系列的记号(Token),说白了就是简单地将代码进行分割和整理。
比如测试代码array[index] = (index + 4) * (2 + 6)
,这行代码一共有28个非空字符,经过扫描后,可以产生16个记号,如下表:
记号 | 类型 |
---|---|
array | 标识符 |
[ | 左方括号 |
index | 标识符 |
] | 右方括号 |
= | 赋值 |
( | 左圆括号 |
index | 标识符 |
+ | 加号 |
4 | 数字 |
) | 右圆括号 |
* | 乘号 |
( | 左圆括号 |
2 | 数字 |
+ | 加号 |
6 | 数字 |
) | 右圆括号 |
词法分析产生的记号分为关键字、标识符、字面量(包含数字、字符串等)和特殊符号(加号、乘号等),在识别记号的同时,扫描器也完成了一些其他动作,比如将标识符放到符号表中,将数字、字符串常量放入到文字表中。
由此可见,这个词法分析的规则也是不同语言有着不同的规定,比如这里的array
并不会分析判断为arr
和ay
这2个标记符,同样的对于括号都有着特殊匹配规则,当写出不符合词分析的代码时,在这一步便会报错。
语法分析
词法分析完后就是进行语法分析,由语法分析器(Grammer Parser)对记号进行语法分析,产生语法树(Syntax Tree)。这一步非常重要,也是我们学习一门新语言时经常会出现IDE提示语法错误,而如何判断语法错误,就是这一步分析的。
首先有个概念就是一条语句是一个表达式,而复杂的语句由多个表达式组合起来的,比如上面那行代码就是由赋值表达式、加法表达式、乘法表达式、数组表达式、括号表达式组成的复杂表达式。
这时就有个关键问题,就是表达式是具有优先级的。上面示例代码中,我们肯定知道核心是一个赋值表达式,先把赋值表达式左右都计算完,再进行赋值操作。但是这是建立在我们学过编程的前提下,当对于分析器来说,不给它设定规则,它就无法知道是先计算乘法还是先进行赋值操作。
不仅仅是运算符优先级,还包括多重含义符号的区分,比如*
,包括表达式是否合法,比如缺少括号、操作符缺少参数等,这些编译器都会报错。
语法分析进行完后,至少可以判断我们写的代码有没有语法错误,而有兴趣的可以查看一下语法分析的算法和规则,这样可以更加理解语法分析过程,帮助我们平时碰到语法错误时,能快速找到原因。
语义分析
经过语法分析后的语法树基本上能对简单的错误进行判断和提示了,比如运算符缺少参数等,但是语义错误无法判断,这就需要语义分析器来进行语义分析了。
什么是语义呢?就是这个语句是否有意义,比如在C语言中,2个指针做乘法是没有意义的,但是在语法层面是可行的。这种编译器能分析的语义是静态语义(Static Semantic),与之对应的是动态语义(Dynamic Semantic),就是只能在运行期才能确定是否非法的语义,比如除零操作。
静态语义分析有个非常重要的的作用是声明和类型的匹配,以及类型的转换。对于声明和类型匹配,对于C++这种复杂的编程语言来说,是经常写错的,尤其是复合类型和函数类型。而类型转换一般就是隐式类型转换,比如把一个浮点型的表达式赋值给一个整型的表达式,就隐含了一个浮点型转到整型的过程。
经过语义分析后的语法树,每个表达式都有了固定的类型,很多编程语言都是静态类型语言,在这个阶段后要不我们自己声明要不编译器推导,整个语法树的表达式都会被标识上类型。
中间代码生成
现代编译器有着很多的优化,其中一个就是源码级别会有一个优化过程。比如上面代码中(2+8)
就可以被优化掉,因为它的值已经被确定了。
除了这种,还有一些常见的优化:
比如代码:
int square(int x) {
int result = x * x;
return result;
}
这里的变量result
可能就会被编译器给优化掉,直接返回x * x
;
比如代码:
inline int square(int x) {
return x * x;
}
int calculateSquare(int y) {
return square(y); // 内联函数调用
}
比如这里在调用square()
方法时,会直接把其函数体复制到调用处,以减少函数调用带来的额外开销。
int calculateExpression(int x, int y) {
int z = x * 5 + y * 5; // 公共子表达式
int w = x * 5 + y * 5; // 公共子表达式
return z + w;
}
在这里公共的子表达式就是x *5 + y * 5
,编译器会把其的值缓存起来,进行一次计算即可。
constexpr int factorial(int n) {
return (n