# 多文件项目 ## 简介 一个软件项目往往包含多个源码文件,编译时需要将这些文件一起编译,生成一个可执行文件。 假定一个项目有两个源码文件`foo.c`和`bar.c`,其中`foo.c`是主文件,`bar.c`是库文件。所谓“主文件”,就是包含了`main()`函数的项目入口文件,里面会引用库文件定义的各种函数。 ```c // File foo.c #include int main(void) { printf("%d\n", add(2, 3)); // 5! } ``` 上面代码中,主文件`foo.c`调用了函数`add()`,这个函数是在库文件`bar.c`里面定义的。 ```c // File bar.c int add(int x, int y) { return x + y; } ``` 现在,将这两个文件一起编译。 ```bash $ gcc -o foo foo.c bar.c # 更省事的写法 $ gcc -o foo *.c ``` 上面命令中,gcc 的`-o`参数指定生成的二进制可执行文件的文件名,本例是`foo`。 这个命令运行后,编译器会发出警告,原因是在编译`foo.c`的过程中,编译器发现一个不认识的函数`add()`,`foo.c`里面没有这个函数的原型或者定义。因此,最好修改一下`foo.c`,在文件头部加入`add()`的原型。 ```c // File foo.c #include int add(int, int); int main(void) { printf("%d\n", add(2, 3)); // 5! } ``` 现在再编译就没有警告了。 你可能马上就会想到,如果有多个文件都使用这个函数`add()`,那么每个文件都需要加入函数原型。一旦需要修改函数`add()`(比如改变参数的数量),就会非常麻烦,需要每个文件逐一改动。所以,通常的做法是新建一个专门的头文件`bar.h`,放置所有在`bar.c`里面定义的函数的原型。 ```c // File bar.h int add(int, int); ``` 然后使用`include`命令,在用到这个函数的源码文件里面加载这个头文件`bar.h`。 ```c // File foo.c #include #include "bar.h" int main(void) { printf("%d\n", add(2, 3)); // 5! } ``` 上面代码中,`#include "bar.h"`表示加入头文件`bar.h`。这个文件没有放在尖括号里面,表示它是用户提供的;它没有写路径,就表示与当前源码文件在同一个目录。 然后,最好在`bar.c`里面也加载这个头文件,这样可以让编译器验证,函数原型与函数定义是否一致。 ```c // File bar.c #include "bar.h" int add(int a, int b) { return a + b; } ``` 现在重新编译,就可以顺利得到二进制可执行文件。 ```bash $ gcc -o foo foo.c bar.c ``` ## 重复加载 头文件里面还可以加载其他头文件,因此有可能产生重复加载。比如,`a.h`和`b.h`都加载了`c.h`,然后`foo.c`同时加载了`a.h`和`b.h`,这意味着`foo.c`会编译两次`c.h`。 最好避免这种重复加载,虽然多次定义同一个函数原型并不会报错,但是有些语句重复使用会报错,比如多次重复定义同一个 Struct 数据结构。解决重复加载的常见方法是,在头文件里面设置一个专门的宏,加载时一旦发现这个宏存在,就不再继续加载当前文件了。 ```c // File bar.h #ifndef BAR_H #define BAR_H int add(int, int); #endif ``` 上面示例中,头文件`bar.h`使用`#ifndef`和`#endif`设置了一个条件判断。每当加载这个头文件时,就会执行这个判断,查看有没有设置过宏`BAR_H`。如果设置过了,表明这个头文件已经加载过了,就不再重复加载了,反之就先设置一下这个宏,然后加载函数原型。 ## extern 说明符 当前文件还可以使用其他文件定义的变量,这时要使用`extern`说明符,在当前文件中声明,这个变量是其他文件定义的。 ```c extern int myVar; ``` 上面示例中,`extern`说明符告诉编译器,变量`myvar`是其他脚本文件声明的,不需要在这里为它分配内存空间。 由于不需要分配内存空间,所以`extern`声明数组时,不需要给出数组长度。 ```c extern int a[]; ``` 这种共享变量的声明,可以直接写在源码文件里面,也可以放在头文件中,通过`#include`指令加载。 ## static 说明符 正常情况下,当前文件内部的全局变量,可以被其他文件使用。有时候,不希望发生这种情况,而是希望某个变量只局限在当前文件内部使用,不要被其他文件引用。 这时可以在声明变量的时候,使用`static`关键字,使得该变量变成当前文件的私有变量。 ```c static int foo = 3; ``` 上面示例中,变量`foo`只能在当前文件里面使用,其他文件不能引用。 ## 编译策略 多个源码文件的项目,编译时需要所有文件一起编译。哪怕只是修改了一行,也需要从头编译,非常耗费时间。 为了节省时间,通常的做法是将编译拆分成两个步骤。第一步,使用 GCC 的`-c`参数,将每个源码文件单独编译为对象文件(object file)。第二步,将所有对象文件链接在一起,合并生成一个二进制可执行文件。 ```bash $ gcc -c foo.c # 生成 foo.o $ gcc -c bar.c # 生成 bar.o # 更省事的写法 $ gcc -c *.c ``` 上面命令为源码文件`foo.c`和`bar.c`,分别生成对象文件`foo.o`和`bar.o`。 对象文件不是可执行文件,只是编译过程中的一个阶段性产物,文件名与源码文件相同,但是后缀名变成了`.o`。 得到所有的对象文件以后,再次使用`gcc`命令,将它们通过链接,合并生成一个可执行文件。 ```bash $ gcc -o foo foo.o bar.o # 更省事的写法 $ gcc -o foo *.o ``` 以后,修改了哪一个源文件,就将这个文件重新编译成对象文件,其他文件不用重新编译,可以继续使用原来的对象文件,最后再将所有对象文件重新链接一次就可以了。由于链接的耗时大大短于编译,这样做就节省了大量时间。 ## make 命令 大型项目的编译,如果全部手动完成,是非常麻烦的,容易出错。一般会使用专门的自动化编译工具,比如 make。 make 是一个命令行工具,使用时会自动在当前目录下搜索配置文件 makefile(也可以写成 Makefile)。该文件定义了所有的编译规则,每个编译规则对应一个编译产物。为了得到这个编译产物,它需要知道两件事。 - 依赖项(生成该编译产物,需要用到哪些文件) - 生成命令(生成该编译产物的命令) 比如,对象文件`foo.o`是一个编译产物,它的依赖项是`foo.c`,生成命令是`gcc -c foo.c`。对应的编译规则如下: ```c foo.o: foo.c gcc -c foo.c ``` 上面示例中,编译规则由两行组成。第一行首先是编译产物,冒号后面是它的依赖项,第二行则是生成命令。 注意,第二行的缩进必须使用 Tab 键,如果使用空格键会报错。 完整的配置文件 makefile 由多个编译规则组成,可能是下面的样子。 ```c foo: foo.o bar.o gcc -o foo foo.o bar.o foo.o: bar.h foo.c gcc -c foo.c bar.o: bar.h bar.c gcc -c bar.c ``` 上面是 makefile 的一个示例文件。它包含三个编译规则,对应三个编译产物(`foo.o`、`bar.o`和`foo`),每个编译规则之间使用空行分隔。 有了 makefile,编译时,只要在 make 命令后面指定编译目标(编译产物的名字),就会自动调用对应的编译规则。 ```bash $ make foo.o # or $ make bar.o # or $ make foo ``` 上面示例中,make 命令会根据不同的命令,生成不同的编译产物。 如果省略了编译目标,`make`命令会执行第一条编译规则,构建相应的产物。 ```bash $ make ``` 上面示例中,`make`后面没有编译目标,所以会执行 makefile 的第一条编译规则,本例是`make foo`。由于用户期望执行`make`后得到最终的可执行文件,所以建议总是把最终可执行文件的编译规则,放在 makefile 文件的第一条。makefile 本身对编译规则没有顺序要求。 make 命令的强大之处在于,它不是每次执行命令,都会进行编译,而是会检查是否有必要重新编译。具体方法是,通过检查每个源码文件的时间戳,确定在上次编译之后,哪些文件发生过变动。然后,重新编译那些受到影响的编译产物(即编译产物直接或间接依赖于那些发生变动的源码文件),不受影响的编译产物,就不会重新编译。 举例来说,上次编译之后,修改了`foo.c`,没有修改`bar.c`和`bar.h`。于是,重新运行`make foo`命令时,Make 就会发现`bar.c`和`bar.h`没有变动过,因此不用重新编译`bar.o`,只需要重新编译`foo.o`。有了新的`foo.o`以后,再跟`bar.o`一起,重新编译成新的可执行文件`foo`。 Make 这样设计的最大好处,就是自动处理编译过程,只重新编译变动过的文件,因此大大节省了时间。