2.4 单独编译文件

如果程序存储在一个文件中任何单独函数的改动都会导致整个程序程序编译来产生新的可执行文件。重新编译大文件是非常耗费时间的。

当程序存储在多个文件中时只有那个代码被改变的文件会被重新编译。这种方法分为两个阶段:源文件分开编译然后一起链接。在第一个阶段中文件编译后产生的文件不是可执行文件而是文件后缀名为'.o'(使用GCC编译器时)的文件被称为目标文件。

在第二个阶段中目标文件被连接器合并到一起。连接器将所有目标文件合并创建为一个可执行文件。

一个目标文件中包含机器码,所有其他文件中函数引用的内存地址都被视为未定义。这样允许源文件在编译时不需要直接相互引用。连接器在生成可执行文件时会补充这些丢失的内存地址。

2.4.1 源文件生成目标文件

'-c'命令行选项用于编译源文件为目标文件。例如下面的命令会编译源文件'main.c'为一个目标文件:

$ gcc -Wall -c main.c

生成的目标文件'mian.o'包含mian函数的机器码。也包含外部函数hello的一个引用但是对应的内存地址在这个阶段还没有定义。

相应的命令编译'hello_fn.c'文件中的hello函数:

$ gcc -Wall -c hello_fn.c

生成目标文件'hello_fn.o'。

注意在这种情况下不需要使用'-o'选项指定输出文件名。当使用'-c'选项时编译器自动创建和源文件同名的目标文件然后跟上'.o'后缀。

没有必要将'hello.h'头文件包含在命令行中因为'main.c'和'hello_fn.c'文件中的#include声明会自动包含此头文件。

2.4.2 使用目标文件创建可执行文件

使用gcc生成可执行文件的最后一个步骤是链接所有目标文件,填充外部函数的地址。使用下面命令链接目标文件:

$ gcc main.o hello_fn.o -o hello

这里不需要使用'-Wall'警告选项因为源文件已经成功的被编译为目标文件。一旦源文件编译成功,链接是非常明确的过程要么成功要么失败(当引用不能被解决时会导致失败)。

gcc使用一个单独的程序--连接器ld来完成链接工作。在GNU操作系统中使用GNU链接器和GNU ld,在其他操作系统中可能使用GNU链接器或者使用它们自己的链接器。链接器会在后面讨论(请看第11章[编译器时怎样工作的])链接器运行时gcc使用目标文件创建可执行文件。

可执行文件现在可以运行:

$ ./hello

Hello, world!

它运行的结果和前面章节中使用一个源文件的版本是一样的。

2.4.3 目标文件的连接顺序

在类Unix系统中编译器和链接器的传统行为是从命令行指定的目标文件中从左到右查找外部函数。这意味着含有函数定义的目标文件应该出现在任何调用这个函数的文件之后。

在这个例子中含有hello函数的文件'hello_fn.o'应该在'main.o'的文件后面指定因为main调用了hello函数:

$ gcc main.o hello_fn.o -o hello (正确的顺序)

有些编译器或者链接器如果指定文件的顺序错误会导致编译失败

$ cc hello_fn.o main.o -o hello (错误的顺序)

main.o: In function ‘main’:

main.o(.text+0xf): undefined reference to ‘hello’

因为'main.o'后面没有目标文件包含hello函数。

大多数现代编译器和链接器会查找所有目标文件不依赖与文件顺序,但是由于不是所有编译器会这样做所以最好的办法是按照规定的顺序从左到右排列目标文件。

这是非常值得注意的当你遇到没有被定义的引用这样的问题并且所有必要的目标文件都已经在命令行中。