这可能是比您想要的更详细的答案,但我认为一个体面的解释是合理的。
在 C 和 C++ 中,一个源文件被定义为一个翻译单元。按照惯例,头文件包含函数声明、类型定义和类定义。实际的功能实现驻留在翻译单元中,即 .cpp 文件。
这背后的想法是函数和类/结构成员函数被编译和组装一次,然后其他函数可以从一个地方调用该代码而不会重复。您的函数被隐式声明为“extern”。
/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);
/* function body, or function definition. */
int add(int a, int b)
{
return a + b;
}
如果您希望翻译单元的函数是本地的,则将其定义为“静态”。这是什么意思?这意味着如果你包含带有外部函数的源文件,你会得到重新定义错误,因为编译器不止一次遇到相同的实现。因此,您希望所有翻译单元都看到函数声明,而不是函数体。
那么这一切最后是如何融合在一起的呢?那是链接器的工作。链接器读取汇编器阶段生成的所有目标文件并解析符号。正如我之前所说,符号只是一个名称。例如,变量或函数的名称。当调用函数或声明类型的翻译单元不知道这些函数或类型的实现时,这些符号被称为未解析。链接器通过将保存未定义符号的翻译单元与包含实现的翻译单元连接在一起来解析未解析的符号。呸。对于所有外部可见符号都是如此,无论它们是在您的代码中实现还是由附加库提供。库实际上只是一个包含可重用代码的档案。
有两个值得注意的例外。首先,如果你有一个小函数,你可以让它内联。这意味着生成的机器代码不会生成外部函数调用,而是就地串联。由于它们通常很小,因此大小开销无关紧要。您可以想象它们的工作方式是静态的。所以在头文件中实现内联函数是安全的。类或结构定义中的函数实现通常也由编译器自动内联。
另一个例外是模板。由于编译器在实例化它们时需要查看整个模板类型定义,因此不可能像使用独立函数或普通类那样将实现与定义分离。好吧,也许现在这是可能的,但是要获得对“export”关键字的广泛编译器支持需要很长时间。因此,如果不支持“导出”,翻译单元将获得自己的实例化模板类型和函数的本地副本,类似于内联函数的工作方式。在支持“导出”的情况下,情况并非如此。
对于这两个例外,有些人发现将内联函数、模板化函数和模板化类型的实现放在 .cpp 文件中,然后 #include .cpp 文件“更好”。这是头文件还是源文件并不重要。预处理器不在乎,只是一个约定。
从 C++ 代码(几个文件)到最终可执行文件的整个过程的快速总结:
- 运行预处理器,它解析所有以“#”开头的指令。例如,#include 指令将包含的文件与劣等文件连接起来。它还进行宏替换和令牌粘贴。
- 实际编译器在预处理器阶段之后在中间文本文件上运行,并发出汇编代码。
- 汇编程序在汇编文件上运行并发出机器代码,这通常称为目标文件,并遵循相关操作系统的二进制可执行格式。例如,Windows 使用 PE(可移植的可执行格式),而 Linux 使用带有 GNU 扩展的 Unix System V ELF 格式。在这个阶段,符号仍然被标记为未定义。
- 最后,运行链接器。所有之前的阶段都按顺序在每个翻译单元上运行。但是,链接器阶段适用于由汇编器生成的所有生成的目标文件。链接器解析符号并执行许多魔术,例如创建节和段,这取决于目标平台和二进制格式。程序员通常不需要知道这一点,但在某些情况下它肯定会有所帮助。
同样,这绝对超出了您的要求,但我希望细节能帮助您看到更大的图景。