对于C/C++程序员来说,写完程序之后我们通常说的一句话就是”把程序编译一下“,编译完成后就生成了可执行程序,因此我们误以为从源代码到可执行性程序只需要编译这一步,实际上这是不正确的,这样理解忽略了从源文件到可执行程序中的重要一步,那就是链接,link。
编译实际上仅仅将源代码转为了二进制机器指令,保存这些二进制机器指令的文件叫做
目标文件
。
注意,这还不是最终的可执行程序,每个源文件对应一个目标文件,你写了a.c和b.c和c.c那么经编译器编译后会生成3个目标文件。
现在我们得到了一堆目标文件,这些目标文件最终是怎么变成一个可执行程序了呢?
当然,除了打包目标文件之外,链接器默认还会打包另一个非常重要的东西,那就是标准库,以C语言为例,那就是C标准库:
所以忽略中间结果,这里的目标文件就是中间结果,只从源头看,我们能发现最终的可执行程序来自两部分:我们写的代码与标准库,这两部分组成了最终的可执行程序。
看到这里有的同学可能会问,那链接器的作用看起来很简单,不就是个打包工具吗,之前提打包这个词只是为了好理解,实际上链接器最重要的工作就是决定符号真的有定义,以及该用哪个定义,这里的符号指的是变量名或者函数名。
int main() {
printf("hello world!\n");
};
想必任何一个学过C语言的同学对此都不会陌生,我们将这段代码保存为hello.c。
实际上编译器在编译hello.c遇到printf时根本就不知道printf这个符号定义在哪里,这不是编译器该关心的事情。
因此我们可以看到编译器只能看到局部,看不到全局,这里的局部就局限在一个源文件内部。
那么到底是谁来关心printf定义在哪里呢?这就是链接器,不要忘了,链接器要打包所有目标文件,因为链接器可以看到全局,链接器具有上帝视角。
由于我们只写了一个hello.c,因此最终只会生成一个目标文件,hello.c中没有定义printf,那么printf还能定义在哪里呢?
当然,如果链接器翻遍所有目标文件以及标准库都没有找到某个符号的定义那么就会报一个经典错误,那就是undefined reference to `func'。
void func(int a);
void main() {
func(1);
}
接下来我们编译一下,可以看到这里实际上是没有编译错误的,因为编译器发现你在调用一个定义在外部模块的函数叫做abc的函数,而你的使用方法也是没有问题的,因此编译通过。
但连接器在打包时翻遍所有目标文件以及标准库也没有找到一个叫做abc的函数,因此开始抱怨它找不到这个符号的定义,这就是为什么它会报错的原因。