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