一个 C++ 工程,要从源码生成可执行程序,然后去执行。
可执行文件的生成有时也直接称"编译"(毕竟或者所以这个过程的执行工具就叫做编译器),它实际包含4个步骤,预处理、编译、汇编、链接。所以编译可以有两层意思,一是源代码到可执行文件生成过程的统称,二是这个过程中的第二步,即编译的第二步是编译(:。这四步各做了一些比较重要的事情需要我们了解。预处理过程执行预处理命令,比如 #if#define
等,所以 #define
宏的文本替换发生在这一阶段。编译过程会对每个源文件进行词法分析、语法分析、语义分析,生成汇编代码,这个过程会进行代码优化,以及模板的处理。所以一些高性能计算库喜欢用模板元编程,将部分计算过程放到编译阶段,以增加编译时长为代价优化运行耗时。汇编过程生成目标文件,是将汇编代码翻译成机器码,这个过程几乎不会做优化,几乎是一对一的映射。链接过程将目标文件链接到一起,所以静态链接就发生在这一阶段。而动态链接,是在可执行文件执行的过程中进行链接,此时可执行文件早已生成,所以动态链接库不会集成到可执行文件中。
编译的这4步各有对应的工具,为了使用方便,通常将他们集成到一起,用参数控制使用哪个工具来做对应的事情。这个集成到一起的工具就是编译器,比如 gcc g++ clang clang++。不同的编译器有什么区别呢,主要区别在于硬件兼容性、编译性能和代码优化上。编译器的质量直接影响生成的可执行文件的执行效率。
有上面的过程,看起来源代码生成可执行文件的事情已经解决了。但实际上,当项目工程逐渐庞大起来之后,事情变得越来越复杂,直接使用编译命令编写编译脚本非常困难,更何况很多模块编译的时候是可以并行编译出目标文件,最后再链接到一起,这可以大大加快编译过程。因此,就出现了编译器之上的构建工具。构建工具定义了自己的构建脚本语法,比如 make 的 Makefile 文件,在一定程度上简化了编译脚本的编写,不必敲写裸命令了。但构建工具最主要做的是优化编译过程的事情,也就是缩短编译时间。常用的构建工具有make、nmake、ninja。构建工具可以指定使用哪个编译器,比如 make 可通过修改 Makefile 文件的 CC=gcc
CXX=g++
来指定使用 gcc g++ 执行编译。
随着项目工程进一步庞大,尤其是当工程模块之间的依赖错综复杂起来之后,构建工具的脚本也复杂了起来,维护困难。毕竟构建工具设计之初一项最重要的任务是优化编译过程,脚本语言设计得没那么傻瓜。因此,就在构建工具之上又加了一层,这就是 CMake。 CMake 是用来生成其他构建工具的构建脚本的,它的关注点在跨平台和易用性。所以,CMake 可以指定使用哪个构建工具,以及使用哪个编译器。
这些过程可以用下面的图例表示。
另外,还有一个编译工具链的概念。它包含的范围有两种说法,一种是源代码到可执行文件这个生成过程中所涉及的所有工具,也就是用更专业的"编译工具链"来指代前文所说的"编译器",另一种认为不仅包含这些工具,还包含构建工具。这里比较倾向第一种说法,因为只有这个过程影响可执行代码的质量,影响程序的运行效率,其他构建的过程只影响编译速度和维护难度。项目中,我们更重视前者。
理解这些概念和过程,有助于我们知晓配置开发环境时的每一步在做什么,有没有落下什么,以及出问题时理解报错信息,明确排查和解决方向。
比如 在 Windows 下基于 Visual Studio Code 使用 CMake + MinGW 配置 C++ 开发环境