从上篇文章中,我们可以看到一点头文件的作用:就是声明各个函数或变量,以供调用;而至于函数或变量的本体,在链接阶段补上。在main.c中。我们手动声明了两个函数,但其实这样比较费力不讨好,因为如果还有很多其他文件也需要调用这两个函数,那么也要在那些文件中一次次的声明;两个函数还好,如果是成千上百个呢?还要一个一个的去声明吗?这时候,头文件就是一个更好的选择:只要把那些需要用到的函数或变量写进头文件,然后include这个头文件就可以了。头文件就是声明的替代,或者说是批量的声明。
我们的头文件file1.h可以这样写:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
extern int gen_rnd(void);
extern void judge(int, int);
以前我在写代码的时候,经常会有这样的疑问:我能不能调用另一个文件中的函数?或者我该如何调用另一个文件中的函数?是不是只要“另一个文件”在同一目录下,我就可以随意调用它的代码?不是有所谓的外链吗?
现在终于有了答案:如果想要调用另一个文件的函数或变量,首先被调用的函数或变量必须先声明为具有外链的(详见我介绍存储类的那篇文章);然后在本文件中声明相应的函数或变量(包括引用相关头文件);最后,要链接相应的目标文件。这样,我们就完成了所谓的跨文件调用。
我在介绍存储类的那篇文章中提到过,函数声明只要不被static声明,都是具有外链的(extern可以被省略);而想要变量具有外链,必须将其声明在所有函数之外(即使其具有文件作用域),同时用关键字extern修饰。这样以来,这些函数或变量除了在定义所在的文件中有声明外(定义本身即是一种声明),在所有引用它们的文件中也会有一份对应的声明,这些“备份”声明的作用就是告诉引用它们的文件:被声明的函数或变量,是同一个函数或变量,在完成链接之后指向相同的地址。因而,在我们的例子中,main.c声明了judge()函数,就不能再定义一个同名的函数了,除非决定不再链接judge.o。
下面在谈一谈如何寻找头文件以及如何根据头文件(或声明)在链接阶段寻找对应的目标文件。#include命令后面的头文件其实分为两类,一种是用尖括号扩起来的,而另一种是用双引号扩起来的。在我们的例子中,假设头文件file1.h放在main.c所在的目录,那么编译main.c的命令是这样的:
gcc -s main.c
gcc会自动在main.c所在的目录搜索头文件。如果file1.h放在main.c所在的目录的一个子目录sub中,那么编译命令是:
gcc -s main.c -I sub
-I 选项用来指定头文件的搜索路径,这个路径是被编译文件的相对路径,起始目录就是被编译文件所在目录。当然,这个相对路径也可以体现在#include命令中,比如,#include "sub/file1.h" 那么编译的时候就不需要-I选项了。
对于用角括号包含的头文件, gcc 首先查找 -I 选项指定的目录,然后查找系统的头文件目录(通常是 /usr/include );而对于用引号包含的头文件, gcc 首先查找包含头文件的 .c 文件所在的目录,然后查找 -I 选项指定的目录,然后查找系统的头文件目录。
指定头文件的路径是为了在编译的时候生成正确的声明,声明有了,如何去找到对应的目标文件呢?对于自己写的头文件,当然是自己在链接的时候给出路径来;而对于用尖括号括起来的头文件呢?gcc会自动去寻找相应的库文件,比如printf函数就在libc库中。这些标准库文件的路径都是系统固定的,gcc会默认去这些目录搜索,编译器默认会找的目录可以用 -print-search-dirs 选项查看。
那么库文件(暂时只考虑静态库)和目标文件有啥区别?其实我们也可以把刚才的judge.o和gen_rnd.o做成一个静态库:
$ ar rs libgame.a judge.o gen_rnd.o
库文件名都是以 lib 开头的,静态库以 .a 作为后缀,表示Archive。 ar 命令类似于 tar 命令,起一个打包的作用,但是把目标文件打包成静态库只能用 ar 命令而不能用 tar 命令。选项 r 表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。 s 是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。
然后我们可以这样编译main.c:
gcc main.c -L . -I game -o main
-L 选项告诉编译器去哪里找需要的库文件,-L. 表示在当前目录找, -l (小写的L)选项告诉编译器要链接libgame库.注意,即使库文件就在当前目录,编译器默认也不会去找的,所以 -L选项不能少。
那为什么要把目标文件做成库文件呢?首先,如果有太多目标文件的话,gcc命令会敲的手疼(⊙﹏⊙b汗),而库文件的编译命令就很简洁;其次,假设我们又在judge.c中添加了一个无关的add函数,那么直接链接目标文件,会把这些无关代码也加进可执行文件中,于是如果无关函数很多的话,就是使得可执行文件变得很大,但是如果链接库文件的话,链接器可以从静态库中只取出需要的部分来做。
最后留个尾巴:为什么不直接include “judge.c”等那些源文件呢?