首先,从高层次上了解 C++ 程序是如何编译的。如果您的项目中有三个文件 - main.cpp
、foo.cpp
和foo.h
- 您可以像这样编译项目:
g++ main.cpp foo.cpp
这两个文件是分开编译的。预处理器和编译器根本不关心这些文件之间的关系。预处理和编译main.cpp
产生一个目标文件,预处理和编译foo.cpp
产生另一个目标文件。
只有在最后一步,即链接步骤中,才会考虑关系。链接器将看到它的main
定义main.cpp
并将其作为程序的起点。链接器将看到里面有一个对定义main.cpp
的函数的调用,并将它们链接在一起。foo
foo.cpp
因此,对于您传递给编译器的每个文件,有两个主要步骤:
预处理 - 以非常原始的方式处理文件,从而产生预处理令牌。其中一些标记是以 开头的预处理指令#
,例如#include
或#define
。这些指令被执行。执行#include
指令几乎只是将命名文件的内容复制到当前文件中。
预处理阶段产生所谓的翻译单元。
编译——然后对文件进行句法和语义分析并翻译成目标文件。这就是它开始关心您的代码究竟要做什么以及它是否正确的地方。如果您尝试使用尚未声明的名称,编译器会告诉您。但是,您通常可以使用尚未定义的名称,因为它们可能在另一个翻译单元中定义。
此编译阶段生成目标文件。
在此之后,通过解析它们之间的引用来组合生成的目标文件。如果对应的目标文件main.cpp
使用了foo
定义的函数,foo.cpp
那么它们将链接在一起,依此类推。
为了更清楚,让我们看一个例子。假设我们有以下源文件:
所以我们在命令行上将main.cpp
and传递给编译器。foo.cpp
请注意,我们不会将头文件传递给编译器。它们仅包含在.cpp
文件中。另请注意,.cpp
文件通常不包含其他.cpp
文件。每个.cpp
文件都单独编译并稍后链接。
所以在预处理阶段之后,我们有两个翻译单元:
请注意, 的内容foo.h
已被复制到每个文件中。我们现在有两个完全有效的翻译单元。第一个只是定义main
函数并调用仅声明的foo
函数。第二个首先声明foo
然后在之后定义它。
然后编译翻译单元以生成目标文件,通常称为main.o
和foo.o
. 然后,链接器将查看文件中某些未解析的引用。例如,它会找到对foo
in的调用main.o
并查看它尚未定义。所以它会查看其他目标文件,看看是否能找到它,果然,它在foo.o
.