# 多文件项目

网道（WangDoc.com），互联网文档计划

## 简介 #

一个软件项目往往包含多个源码文件，编译时需要将这些文件一起编译，生成一个可执行文件。

假定一个项目有两个源码文件`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.c
    #include "bar.h"

    int add(int a, int b) {
      return a + b;
    }

现在重新编译，就可以顺利得到二进制可执行文件。

    $ gcc -o foo foo.c bar.c

## 重复加载 #

头文件里面还可以加载其他头文件，因此有可能产生重复加载。比如，`a.h`和`b.h`都加载了`c.h`，然后`foo.c`同时加载了`a.h`和`b.h`，这意味着`foo.c`会编译两次`c.h`。

最好避免这种重复加载，虽然多次定义同一个函数原型并不会报错，但是有些语句重复使用会报错，比如多次重复定义同一个 Struct 数据结构。解决重复加载的常见方法是，在头文件里面设置一个专门的宏，加载时一旦发现这个宏存在，就不再继续加载当前文件了。

    // File bar.h
    #ifndef BAR_H
      #define BAR_H
      int add(int, int);
    #endif

上面示例中，头文件`bar.h`使用`#ifndef`和`#endif`设置了一个条件判断。每当加载这个头文件时，就会执行这个判断，查看有没有设置过宏`BAR_H`。如果设置过了，表明这个头文件已经加载过了，就不再重复加载了，反之就先设置一下这个宏，然后加载函数原型。

## extern 说明符 #

当前文件还可以使用其他文件定义的变量，这时要使用`extern`说明符，在当前文件中声明，这个变量是其他文件定义的。

    extern int myVar;

上面示例中，`extern`说明符告诉编译器，变量`myvar`是其他脚本文件声明的，不需要在这里为它分配内存空间。

由于不需要分配内存空间，所以`extern`声明数组时，不需要给出数组长度。

    extern int a[];

这种共享变量的声明，可以直接写在源码文件里面，也可以放在头文件中，通过`#include`指令加载。

## static 说明符 #

正常情况下，当前文件内部的全局变量，可以被其他文件使用。有时候，不希望发生这种情况，而是希望某个变量只局限在当前文件内部使用，不要被其他文件引用。

这时可以在声明变量的时候，使用`static`关键字，使得该变量变成当前文件的私有变量。

    static int foo = 3;

上面示例中，变量`foo`只能在当前文件里面使用，其他文件不能引用。

## 编译策略 #

多个源码文件的项目，编译时需要所有文件一起编译。哪怕只是修改了一行，也需要从头编译，非常耗费时间。

为了节省时间，通常的做法是将编译拆分成两个步骤。第一步，使用 GCC 的`-c`参数，将每个源码文件单独编译为对象文件（object file）。第二步，将所有对象文件链接在一起，合并生成一个二进制可执行文件。

    $ 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`命令，将它们通过链接，合并生成一个可执行文件。

    $ gcc -o foo foo.o bar.o

    # 更省事的写法
    $ gcc -o foo *.o

以后，修改了哪一个源文件，就将这个文件重新编译成对象文件，其他文件不用重新编译，可以继续使用原来的对象文件，最后再将所有对象文件重新链接一次就可以了。由于链接的耗时大大短于编译，这样做就节省了大量时间。

## 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 这样设计的最大好处，就是自动处理编译过程，只重新编译变动过的文件，因此大大节省了时间。

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文