C 编译过程

在开发 C++程序时,下一步是在运行程序之前编译该程序。编译是将用 C,C++等人类可读语言编写的程序转换为中央处理单元直接理解的机器代码的过程。例如,如果你有一个名为 prog.cpp 的 C++源代码文件并执行了 compile 命令,

   g++ -Wall -ansi -o prog prog.cpp

从源文件创建可执行文件涉及 4 个主要阶段。

  1. C++预处理器采用 C++源代码文件并处理头文件(#include),宏(#define)和其他预处理程序指令。

  2. 由 C++预处理器生成的扩展 C++源代码文件被编译为平台的汇编语言。

  3. 编译器生成的汇编代码被组装到平台的目标代码中。

  4. 汇编程序生成
    的目标代码文件与用于生成库或可执行文件的任何库函数的目标代码文件链接在一起。

预处理

预处理器处理预处理器指令,如#include 和#define。它与 C++的语法无关,这就是必须谨慎使用的原因。

它一次在一个 C++源文件上工作,将#include 指令替换为相应文件的内容(通常只是声明),替换宏(#define),并根据#if 选择不同的文本部分, #ifdef 和#ifndef 指令。

预处理器适用于预处理令牌流。宏替换被定义为用其他标记替换标记(操作符##在有意义时允许合并两个标记)。

在所有这些之后,预处理器产生单个输出,该输出是由上述变换产生的令牌流。它还添加了一些特殊的标记,告诉编译器每行的来源,以便它可以使用它们来产生合理的错误消息。

通过巧妙地使用#if 和#error 指令,可以在此阶段产生一些错误。

通过使用下面的编译器标志,我们可以在预处理阶段停止进程。

g++ -E prog.cpp

汇编

编译步骤在预处理器的每个输出上执行。编译器解析纯 C++源代码(现在没有任何预处理器指令)并将其转换为汇编代码。然后调用底层后端(工具链中的汇编程序)将该代码组装成机器代码,生成某种格式的实际二进制文件(ELF,COFF,a.out,…)。此目标文件包含输入中定义的符号的编译代码(以二进制形式)。目标文件中的符号按名称引用。

对象文件可以引用未定义的符号。使用声明时就是这种情况,并且没有为它提供定义。编译器不介意这一点,只要源代码格式正确,编译器就会愉快地生成目标文件。

编译器通常会让你在此时停止编译。这非常有用,因为有了它,你可以单独编译每个源代码文件。这提供的优点是,如果你只更改单个文件,则无需重新编译所有内容。

生成的目标文件可以放在称为静态库的特殊存档中,以便以后重用。

正是在这个阶段,报告了常规编译器错误,如语法错误或失败的重载分辨率错误。

为了在编译步骤之后停止进程,我们可以使用 -S 选项:

g++ -Wall -ansi -S prog.cpp

组装

汇编程序创建目标代码。在 UNIX 系统上,你可能会看到带有 .o 后缀(MSDOS 上的 .OBJ)的文件,以指示目标代码文件。在此阶段,汇编程序将这些目标文件从汇编代码转换为机器级指令,并且创建的文件是可重定位目标代码。因此,编译阶段生成可重定位目标程序,并且该程序可以在不同地方使用而无需再次编译。

要在组装步骤后停止该过程,可以使用 -c 选项:

g++ -Wall -ansi -c prog.cpp

链接

链接器是汇编程序生成的目标文件的最终编译输出。此输出可以是共享(或动态)库(虽然名称类似,但它们与前面提到的静态库没有多少共同之处)或可执行文件。

它通过用正确的地址替换对未定义符号的引用来链接所有目标文件。这些符号中的每一个都可以在其他目标文件或库中定义。如果它们是在标准库以外的库中定义的,则需要告知链接器它们。

在此阶段,最常见的错误是缺少定义或重复定义。前者意味着定义不存在(即它们没有被写入),或者它们所在的目标文件或库没有被赋予链接器。后者是显而易见的:在两个不同的目标文件或库中定义了相同的符号。