对于平常时刻的开发与编程,我们很少会提到编译与链接这个过程。换句话说,这是我们编写程序的时候很少需要注意到的部分,因为我们通常使用的集成开发环境(IDE),比如VS,VScode,Dev,小熊猫等等都已经帮我们隐去了这部分的操作。
但是,这个过程虽然被隐藏,却不能被遗忘。短短一句HELLO WORLD的背后,却是编译器一大段的命令行实现。
本篇内容并非实际功用,而是修炼内功。
(本篇文章参考《程序员的自我修养》,读者可以一步到位直接阅读书籍)
什么是编译和链接?
我们编写任何一个程序,编译器帮我们构建命令行的过程可以分为四个步骤:
预编译(prepressing)、编译(compliation)、汇编(assembly)、链接(linking)
预编译
预编译过程中主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”,“#define”等等。
主要规则如下:(以下为引用)
- 将所有的#define删除,并展开所有的宏定义。
- 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif。
- 处理#include指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释//和/**/。
- 添加行号和文件名标识,比如#2 "hello.c"2,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令,因为编译器须要使用它们。
简单来说,这个阶段就是处理所有#开头的预编译指令。
大部分的预编译指令详见:http://t.csdnimg.cn/wbNow
我这里只给大家讲述一些简单的用法。
(第一行可以忽略,那只是为了scanf不在VS2022上报错)
#define我们很熟悉,是一个宏的用法。
根据刚刚叙述的规则,我们将在后面的程序中展开STR和GRE为100。
然后就是#if #endif这一对好兄弟了。
#if和普通的if别无二致,#if后面的式子也可以是一个表达式(但这个时候就不需要加括号了)。
比较特殊的用法就是这里展现出来的#if defined(STR)。
这里的意思就是STR如果被定义过了,那就可以处理#if到#endif中间的语句。
这也是为什么上一张图片#define GRE 100这个语句不起效果的原因,在#if之前并没有#define STR。
同时,#ifdef可以用于代替#if defined。#elif就是预编译里的else if。
经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件都被插入到.i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认。
编译
编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。这个过程往往是我们所说的整个程序构建的核心,也是最复杂的部分之一。
词法分析

语法分析
有些类似于我们画的语法树和伪代码,语法分析器借由之前扫描器得到的记号进行语法分析,并得到一个独属于机器的逻辑链条(比如只有A才能推到B和C,B只能推到D等等),从而得到加法表达式、乘法表达式等等语句的结合体。
语义分析
由语义分析器完成。之前的语法分析只是得到了机器要怎么一步一步做下去,但是机器并不知道他实际上要做什么,这个“什么”就由语义分析来完成,并且这一步也能知道是否存在着语法合理,但是语义上没有意义的语句。
值得注意的是,编译器所能分析的语义是静态语义,即能在编译期就确定的语义。
对应的动态语义就是只有在运行期才能确定的语义。
静态语义通常包括声明和类型的匹配,类型的转换。比如当一个浮点型表达式赋值给一个整型的表达式,其中隐含了一个浮点型到整型转换的过程。
动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数。
中间语言生成
该步骤是实现优化的一部分。
类似于int i=2+6这样的语句,实际上i的赋值表达式可以被优化为8.
2+6被优化为8直接在语法树上作优化比较困难,因而源代码优化器往往将整个语法树转换为中间代码,与目标代码已经非常相似。
目标代码生成与优化
源代码优化器产生中间代码标志着下面的过程都属于编辑器后端。编译器后端主要包括代码生成器和目标代码优化器。
代码生成器将中间代码转换成目标机器代码,而这因目标机器不同而差别很大。
然后目标代码优化器对上述目标代码进行优化。
但现代编译器有着异常复杂的结构,这是因为现代高级编程语言本身非常地复杂。编译器的指令生成过程非常复杂。
汇编
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。
链接
我们在之前学过函数的使用和编写。在那个时候,我们希望把main函数里的东西分开来写,不仅是为了提高代码复用率,还有就是让我们main函数显的不那么乱,这其中就有了“分治”的思想。将问题简单化为一个个模块分别解决,再拼接起来就得到了我们要的程序。
如果将我们的C语言程序看作一个个模块的话,那我们就需要链接起来,而这就是“链接”这一步得到的东西。
在链接这个阶段之前,编译器在编译时遇到了需要调用的一些函数的地址时,会先搁置这些条用指令,等到链接的阶段再将其用上。而链接器会帮助我们自动引用那些函数或需调用元素的地址。