- 首页
- 作品
- C 语言教程
- 20. C语言的多文件项目
20. C语言的多文件项目
简介
- 一个软件项目往往包含多个源码文件,编译时需要将这些文件一起编译,生成一个可执行文件。
- 假定一个项目有两个源码文件
foo.c
和bar.c
,其中foo.c
是主文件,bar.c
是库文件。所谓“主文件”,就是包含了main()
函数的项目入口文件,里面会引用库文件定义的各种函数。
// File foo.c
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); // 5!
}
- 上面代码中,主文件
foo.c
调用了函数add()
,这个函数是在库文件bar.c
里面定义的。
// File bar.c
int add(int x, int y) {
return x + y;
}
- 现在,将这两个文件一起编译。
$ gcc -o foo foo.c bar.c
# 更省事的写法
$ gcc -o foo *.c
- 上面命令中,gcc 的
-o
参数指定生成的二进制可执行文件的文件名,本例是foo
。
- 这个命令运行后,编译器会发出警告,原因是在编译
foo.c
的过程中,编译器发现一个不认识的函数add()
,foo.c
里面没有这个函数的原型或者定义。因此,最好修改一下foo.c
,在文件头部加入add()
的原型。
// File foo.c
#include <stdio.h>
int add(int, int);
int main(void) {
printf("%d\n", add(2, 3)); // 5!
}
- 现在再编译就没有警告了。
- 你可能马上就会想到,如果有多个文件都使用这个函数
add()
,那么每个文件都需要加入函数原型。一旦需要修改函数add()
(比如改变参数的数量),就会非常麻烦,需要每个文件逐一改动。所以,通常的做法是新建一个专门的头文件bar.h
,放置所有在bar.c
里面定义的函数的原型。
// File bar.h
int add(int, int);
- 然后使用
include
命令,在用到这个函数的源码文件里面加载这个头文件bar.h
。
// File foo.c
#include <stdio.h>
#include "bar.h"
int main(void) {
printf("%d\n", add(2, 3)); // 5!
}
- 上面代码中,
#include "bar.h"
表示加入头文件bar.h
。这个文件没有放在尖括号里面,表示它是用户提供的;它没有写路径,就表示与当前源码文件在同一个目录。
- 然后,最好在
bar.c
里面也加载这个头文件,这样可以让编译器验证,函数原型与函数定义是否一致。
// File bar.h
#include "bar.h"
int add(int, int);
- 现在重新编译,就可以顺利得到二进制可执行文件。
$ gcc -o foo foo.c bar.c
重复加载
extern 说明符
- 当前文件还可以使用其他文件定义的变量,这时要使用
extern
说明符,在当前文件中声明,这个变量是其他文件定义的。
extern int myVar;
- 上面示例中,
extern
说明符告诉编译器,变量myvar
是其他脚本文件声明的,不需要在这里为它分配内存空间。
- 由于不需要分配内存空间,所以
extern
声明数组时,不需要给出数组长度。
extern int a[];
- 这种共享变量的声明,可以直接写在源码文件里面,也可以放在头文件中,通过
#include
指令加载。
static 说明符
- 正常情况下,当前文件内部的全局变量,可以被其他文件使用。有时候,不希望发生这种情况,而是希望某个变量只局限在当前文件内部使用,不要被其他文件引用。
- 这时可以在声明变量的时候,使用
static
关键字,使得该变量变成当前文件的私有变量。
static int foo = 3;
- 上面示例中,变量
foo
只能在当前文件里面使用,其他文件不能引用。
编译策略
make 命令
- 大型项目的编译,如果全部手动完成,是非常麻烦的,容易出错。一般会使用专门的自动化编译工具,比如 make。
- make 是一个命令行工具,使用时会自动在当前目录下搜索配置文件 makefile(也可以写成 Makefile)。该文件定义了所有的编译规则,每个编译规则对应一个编译产物。为了得到这个编译产物,它需要知道两件事。
- 依赖项(生成该编译产物,需要用到哪些文件)
- 生成命令(生成该编译产物的命令)
- 比如,对象文件
foo.o
是一个编译产物,它的依赖项是foo.c
,生成命令是gcc -c foo.c
。对应的编译规则如下:
foo.o: foo.c
gcc -c foo.c
- 上面示例中,编译规则由两行组成。第一行首先是编译产物,冒号后面是它的依赖项,第二行则是生成命令。
- 注意,第二行的缩进必须使用 Tab 键,如果使用空格键会报错。
- 完整的配置文件 makefile 由多个编译规则组成,可能是下面的样子。
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 命令后面指定编译目标(编译产物的名字),就会自动调用对应的编译规则。
$ make foo.o
# or
$ make bar.o
# or
$ make foo
- 上面示例中,make 命令会根据不同的命令,生成不同的编译产物。
- 如果省略了编译目标,
make
命令会执行第一条编译规则,构建相应的产物。
$ 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 这样设计的最大好处,就是自动处理编译过程,只重新编译变动过的文件,因此大大节省了时间。