这可能是比您想要的更详细的答案,但是我认为有一个合理的解释是合理的。
在C和C ++中,一个源文件被定义为一个翻译单元。按照惯例,头文件包含函数声明,类型定义和类定义。实际的函数实现位于翻译单元(即.cpp文件)中。
其背后的思想是,函数和类/结构成员函数仅编译和组装一次,然后其他函数可以从一个位置调用该代码而无需重复。您的函数隐式声明为“外部”。
/* 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;
}
如果您希望某个功能在翻译单元中是本地的,则将其定义为“静态”。这是什么意思?这意味着,如果您包含具有extern函数的源文件,则会遇到重新定义错误,因为编译器多次遇到相同的实现。因此,您希望所有翻译单元都看到函数声明,而不是函数主体。
那么,最后如何将它们融合在一起?那是链接器的工作。链接器读取由汇编程序阶段生成的所有目标文件,并解析符号。正如我之前所说,符号只是一个名称。例如,变量或函数的名称。当调用函数或声明类型的转换单元不知道这些函数或类型的实现时,这些符号就被认为是未解析的。链接器通过将包含未定义符号的转换单元与包含实现的符号单元连接在一起,来解析未解析的符号。ew 无论是在代码中实现还是由其他库提供,所有外部可见符号均适用。库实际上只是具有可重用代码的存档。
有两个值得注意的例外。首先,如果您有一个小功能,可以使其内联。这意味着生成的机器代码不会生成外部函数调用,而是从字面上就位进行连接。由于它们通常很小,因此大小开销无关紧要。您可以想象它们在工作方式上是静态的。因此,在标头中实现内联函数是安全的。类或结构定义中的函数实现通常也由编译器自动内联。
另一个例外是模板。由于编译器在实例化模板时需要查看整个模板类型定义,因此无法像独立函数或普通类那样将实现与定义分离。嗯,也许这是可能的,但是获得广泛的编译器对“ export”关键字的支持花费了很长时间。因此,在不支持“导出”的情况下,翻译单元将获得自己的实例化模板化类型和函数的本地副本,类似于内联函数的工作方式。如果支持“导出”,则不是这种情况。
对于这两个例外,有些人发现将内联函数,模板化函数和模板化类型的实现放入.cpp文件中,然后#include .cpp文件是“更尼克”的事情。这到底是头文件还是源文件并不重要。预处理器不在乎,只是一个约定。
从C ++代码(几个文件)到最终可执行文件的整个过程的快速摘要:
- 运行预处理器,该预处理器解析所有以“#”开头的指令。例如,#include指令将包含的文件与下级连接。它还执行宏替换和令牌粘贴。
- 实际的编译器在预处理程序阶段之后在中间文本文件上运行,并发出汇编代码。
- 在汇编器在汇编文件并发射机代码运行,这通常被称为一个目标文件和如下所讨论的手术系统的二进制可执行的格式。例如,Windows使用PE(便携式可执行格式),而Linux使用带有GNU扩展名的Unix System V ELF格式。在此阶段,符号仍标记为未定义。
- 最后,运行链接器。所有先前阶段均按顺序在每个翻译单元上运行。但是,链接器阶段可处理由汇编器生成的所有生成的目标文件。链接器解析符号,并且执行很多魔术操作,例如创建节和段,这取决于目标平台和二进制格式。程序员一般不需要知道这一点,但是在某些情况下肯定会有所帮助。
同样,这绝对比您要求的要多,但是我希望具体细节可以帮助您看到更大的图景。