到目前为止,我们看的都是些玩具程序,它们绝大部分都能塞进单个文件里。但复杂的 C 程序由许多文件组成,这些文件会被一起编译、链接成一个可执行文件。
本章我们来看看把较大的项目组织起来时,一些常见的模式和做法。
一种非常常见的情况是:你的某些函数定义在一个文件里,而你想从另一个文件调用它们。
这其实开箱即用,只是会带一条警告……我们先试一下,然后再看修正这条警告的正确做法。
要编译它们,你需要在命令行上指定所有源文件:
# 输出文件 源文件
# v v
# |----| |---------|
gcc -o foo foo.c bar.c在那个例子里,foo.c 和 bar.c 会被构建成名为 foo 的可执行文件。
在这些例子里,我们会把文件名写成源代码里的第一条注释。来看看源文件 bar.c:
再看包含 main 的 foo.c 文件:
看到了吗?在 main() 里我们调用了 add()——可 add() 在一个完全不同的源文件里!它在 bar.c 里,而对它的调用却在 foo.c 里!
如果我们这样构建:
gcc -o foo foo.c bar.c就会得到这个错误:
error: implicit declaration of function 'add' is invalid in C99(或者你得到的可能是一条警告。这你可不该无视。在 C 里永远不要无视警告;把它们统统处理掉。)
如果你还记得讲函数原型的那一节,现代 C 已经禁止隐式声明,也没有任何正当理由把它引入新代码。我们应该把它修好。
implicit declaration 的意思是:我们在使用一个函数——这里就是 add()——却没有事先让 C 知道关于它的任何信息。C 想知道它返回什么、接受哪些类型作为实参,诸如此类。
我们前面已经看到如何用函数原型来修正这个问题。确实,只要在调用之前往 foo.c 里加一个函数原型,一切就都正常了:
// 文件 foo.c
#include <stdio.h>
int add(int, int); // 添加函数原型
int main(void)
{
printf("%d\n", add(2, 3)); // 5!
}不再有错误了!
但这很麻烦——每次想用某个函数都得把原型敲一遍。我是说,我们刚才就用了 printf(),也没见需要敲原型啊;这是怎么回事?
其实我们确实包含了 printf() 的原型!它就在 stdio.h 文件里!而我们是使用 #include 把它包含进来的!
我们能不能对自己的 add() 函数也这么做?为它写一个原型,然后放进头文件里?
当然可以!
按照惯例,C 的头文件使用 .h 扩展名。而且它们通常(但并非总是)与对应的 .c 文件同名。所以我们为 bar.c 建一个 bar.h 文件,把原型放进去:
现在我们来修改 foo.c,让它包含那个文件。假设它就在同一目录下,我们用双引号把它包起来包含(而不是用尖括号):
// 文件 foo.c
#include <stdio.h>
#include "bar.h" // 从当前目录包含
int main(void)
{
printf("%d\n", add(2, 3)); // 5!
}注意,foo.c 里已经不再有原型了——我们是从 bar.h 把它包含进来的。现在任何想要那份 add() 功能的文件,只要 #include "bar.h" 就能拿到,你不用再操心去敲函数原型。
你可能已经猜到了,#include 就是字面意义上把指定的文件包含到源代码里的那个位置,就像你自己把它敲进去一样。
然后构建并运行:
./foo
5果然,我们得到了 \(2+3\) 的结果!好耶!
不过先别急着开你那罐心头好。就快好了!我们还差最后一块样板代码要加。
头文件自己再用 #include 包含一些头文件,供对应的 C 文件实现其功能,这并不罕见。我是说,有何不可呢?
而且同一个头文件很可能被不同地方 #include 了多次。也许这没问题,但也可能引发编译器错误。而我们根本控制不了有多少地方会 #include 它!
更糟的是,我们还可能陷入一种疯狂的局面:头文件 a.h 包含头文件 b.h,而 b.h 又包含 a.h!这是一个 #include 的死循环!
试着构建这样的东西会报错:
error: #include nested depth 200 exceeds maximum of 200也许到第 201 步它就能解开这个循环了……
我们需要做的是:让一个文件在被包含过一次之后,后续针对它的 #include 都被忽略。
我们接下来要做的这套东西实在太常见了,常见到你每次新建头文件时都应该条件反射地做一遍!
常见做法是使用一个预处理器变量:第一次 #include 该文件时把它设置好。之后再遇到 #include 时,我们先检查这个变量确实还没有被定义。
至于变量名,非常常见的做法是取头文件的名字,比如 bar.h,改成大写,再把句点换成下划线:BAR_H。
所以在文件的最最开头放一个检查,判断它是否已经被包含过;如果已经包含过,就实际上把整个文件注释掉。
(不要加前导下划线(因为前导下划线后跟大写字母是保留的),也不要加双前导下划线(因为那也是保留的)。)
#ifndef BAR_H // 如果 BAR_H 还没有定义……
#define BAR_H // 定义它(不指定具体值)
// 文件 bar.h
int add(int, int);
#endif // #ifndef BAR_H 的结尾这样一来,无论有多少地方试图 #include 这个头文件,它实际上都只会被包含一次。
static 与 extern
你可以用 extern 引用其他文件中的对象。
用 static 关键字,你可以确保文件作用域的变量和函数不会在其他源文件中可见(即使使用了 extern)。
更多信息请参阅本书中关于 static 和 extern 存储类说明符的章节。
这不属于规范的一部分,但在 C 的世界里 99.999% 的情况下都会这么做。
你可以把 C 文件编译成称为目标文件的中间表示。它们保存着尚未被构建成可执行文件的机器码(也就是实际指令的那些 1 和 0)。
Windows 上的目标文件扩展名是 .OBJ;在类 Unix 系统上是 .o。
在 gcc 中,我们可以用 -c(只编译!)标志这样构建:
gcc -c foo.c # 生成 foo.o
gcc -c bar.c # 生成 bar.o然后我们可以把它们链接成一个可执行文件:
gcc -o foo foo.o bar.o瞧,我们用这两个目标文件生成了可执行文件 foo。
但你在想:何必呢?我们直接这样不就行了:
gcc -o foo foo.c bar.c对小程序来说,这样没问题。我一直都这么干。
但对于较大的程序,我们可以利用这样一个事实:从源代码编译到目标文件相对较慢,而把一堆目标文件链接起来相对较快。
这一点在 make 工具上体现得淋漓尽致,它只重新构建那些比其输出更新的源文件。
假设你有一千个 C 文件。你可以先把它们全部编译成目标文件(慢),然后把所有这些目标文件合并成一个可执行文件(快)。
现在假设你只修改了其中一个 C 源文件——神奇的地方来了:你只需要重新构建那个源文件对应的那一个目标文件!然后重新构建可执行文件(快)。其他所有 C 文件都不必动。
换句话说,只重新构建需要重建的目标文件,我们就能大幅缩短编译时间。(当然,除非你是在做一次“干净”的构建,那样所有目标文件都必须重新生成。)