一个C++程序的编译包括三个步骤:
预处理:预处理器采用 C++ 源代码文件并处理 #includes、#defines 和其他预处理器指令。此步骤的输出是没有预处理器指令的“纯”C++ 文件。
编译:编译器获取预处理器的输出并从中生成目标文件。
链接:链接器获取编译器生成的目标文件并生成库或可执行文件。
预处理
预处理器处理预处理器指令,例如#include 和#define。它与 C++ 的语法无关,因此必须小心使用。
通过将#include 指令替换为相应文件的内容(通常只是声明)、替换宏 (#define) 以及选择文本的不同部分,它一次可处理一个 C++ 源文件取决于#if、#ifdef 和#ifndef 指令。
预处理器处理预处理标记流。宏替换被定义为用其他标记替换标记(运算符## 允许在有意义时合并两个标记)。
在所有这些之后,预处理器产生一个单一的输出,它是由上述转换产生的标记流。它还添加了一些特殊标记,告诉编译器每一行来自哪里,以便它可以使用这些标记来生成合理的错误消息。
在这个阶段巧妙地使用#if 和#error 指令可能会产生一些错误。
编译
编译步骤在预处理器的每个输出上执行。编译器解析纯 C++ 源代码(现在没有任何预处理器指令)并将其转换为汇编代码。然后调用底层后端(工具链中的汇编器),将代码组装成机器代码,以某种格式(ELF、COFF、a.out、...)生成实际的二进制文件。该目标文件包含输入中定义的符号的编译代码(二进制形式)。目标文件中的符号按名称引用。
目标文件可以引用未定义的符号。当您使用声明并且不为其提供定义时就是这种情况。编译器不介意这一点,只要源代码格式正确,它就会愉快地生成目标文件。
编译器通常会让您在此时停止编译。这非常有用,因为您可以使用它单独编译每个源代码文件。这样做的好处是,如果您只更改一个文件,则不需要重新编译所有内容。
生成的目标文件可以放在称为静态库的特殊存档中,以便以后重用。
在此阶段会报告“常规”编译器错误,例如语法错误或重载解析失败错误。
链接
链接器是从编译器生成的目标文件产生最终编译输出的东西。此输出可以是共享(或动态)库(虽然名称相似,但它们与前面提到的静态库没有太多共同之处)或可执行文件。
它通过用正确的地址替换对未定义符号的引用来链接所有目标文件。这些符号中的每一个都可以在其他目标文件或库中定义。如果它们是在标准库以外的库中定义的,则需要将它们告知链接器。
在此阶段,最常见的错误是缺少定义或重复定义。前者意味着定义不存在(即它们没有被写入),或者它们所在的目标文件或库没有提供给链接器。后者很明显:在两个不同的目标文件或库中定义了相同的符号。