4.4 作用域规则
构成一个 C 程序的函数以及外部变量,不需要全都同时编译;程序的源代码可以放在多个源文件中,并且之前编译好的例程可以从库里面加载。需要关心的问题有:
- 要怎么写声明,才能使变量在编译期间被正确声明?
- 要怎么安排声明,才能在程序加载时使程序的所有部分都正确地连接?
- 要怎么组织声明,才能使只有一份拷贝存在?
- 外部变量是如何初始化的?
接下来我们把前面的计算器程序重新组织到多个文件中,以探讨这些主题。实际上,计算器程序太小了,不值得拆分,但它可以很好地说明在更大型程序中会出现的问题。
一个名字的作用域,指的是程序中可以使用该名字的部分。对于在函数开头声明的自动变量,其作用域就是该变量所在的函数。不同函数中相同名字的局部变量之间是无关的。对函数参数来说同样此,因为它们实际上也是局部变量。
外部变量或函数的作用域,在所编译的文件中,从它们声明的地方开始,一直延续到文件末尾。例如,如果 main, sp, val,push 和 pop 在同一个文件中定义,而且顺序也和4.3节中给出的相同,即
main() { ... }
int sp = 0;
double val[MAXVAL];
void push(double f) { ... }
double pop(void) { ... }
则变量 sp 和 val 可以用在 push 和 pop 中,只要简单用它们的名字即可,不需要额外的声明。但 sp 和 val 这两个名字在 main 中是不可见的,push 和 pop 同样也对main不可见。
另一方面,如果外部变量在它定义之前要被引用,或者使用外部变量的源文件不是定义它的源文件,则使用之前必须加上 extern 声明。
区分外部变量的定义和声明是很重要的。声明宣布了变量的属性(主要是它的类型);定义还为变量分配了内存空间。如果下面两行
int sp;
double val[MAXVAL];
出现在所有函数之外,说明它们定义了外部变量 sp 和 val,分配了内存空间,而且这个定义还能作为声明,供该源文件后面的部分使用。另一方面,下面两行
extern int sp;
extern double val[MAXVAL];
对源文件后面的部分声明: sp 是一个 int 而 val 是一个 double 数组(其大小在其他地方确定),但并没有创建变量,或为它们分配内存空间。
在构成一个程序的所有源文件中,一个外部变量只能有一个定义,其他文件可以包含该变量的 extern 声明,用于访问该变量。(在包含外部变量定义的文件中,也可以存在该变量的 extern 声明。)数组大小必须在定义中指定,但在 extern 声明中是可选的。
外部变量的初始化,只能跟着定义一起。
函数 push 和 pop 可以在一个文件中定义,而变量 val 和 sp 在另一个文件中定义并初始化。然后有必要把这些定义和声明绑定在一起:(实际上不太可能这样组织程序,这里只是为了说明)
文件1:
extern int sp;
extern double val[];
void push(double f) { ... }
double pop(void) { ... }
文件2:
int sp = 0;
double val[MAXVAL];
由于文件1中的 extern 声明在所有函数定义的前面,也都在它们的外面,故它们能用于所有函数;这一套声明对文件1完全足够了。如果在一个文件(文件2)中,先使用了 sp 和 val ,之后才定义了 sp 和 val ,那么也需要在文件前面加上这套声明。
4.5 头文件
现在让我们来考虑下,如果计算器程序的各个组成部分都大幅增加,怎么将它分到多个源文件中。main 函数会在一个文件中,我们称之为 main.c; posh、pop 及其变量放到第二个文件中,叫 stack.c;getop 放到第三个文件中,叫 getop.c。最后, getch 和 ungetch 放到第四个文件中,叫 getch.c;我们将它们与其余部分隔开,是因为在实际的程序中,它们可能来自单独编译的库【而不是和其他部分一起编译】。
还有一件事需要担心——文件之间共享的定义和声明。我们想将其尽可能地集中化,这样就只需要写好一份拷贝,而当程序进化时也只需要保证一份拷贝正确就够了。因此,我们将把这个公共资料放在一个头文件里面,叫做 calc.h,有需要就引入。(#include 行在第4.11节描述。)结果程序就像这样:
在“让每个文件只访问它所需的信息”的渴望与“越多头文件越难维护”的现实之间,两者有个权衡。在程序达到一定规模之前,最好的方式可能就是用一个头文件包含程序中所有任意两部分之间需要共享的所有内容;本节我们就是这么做的。对一个更大的程序来说,代码需要更多的组织和更多的头文件。
4.6 静态变量
stack.c 中的变量 sp 和 val,以及 getch.c 中的 buf 和 bufp,是它们各自所在源文件中的函数私有的,并不想要被程序其他部分访问。用于外部变量或函数的 static 静态声明,把对象的作用域限制到所编译源文件的剩余部分。这样,外部的 static 就提供了一种方式来隐藏 getch-ungetch 组合中像 buf 和 bufp 这样的名字:它们必须是外部的,这样才能被共享,然而对 getch 和 ungetch 的用户却不可见。
静态存储通过在正常的声明之前加上 static 前缀来指定。如果两个例程和两个变量在一个文件内编译,如
static char buf[BUFSIZE] /* ungetch的缓存 */
static int bufp = 0; /* buf的下一个空闲位置 */
int getch(void) { ... }
void ungetch(int c) { ... }
则没有其他例程能够访问 buf 和 bufp,而且它们的名字不会与同一程序内其他文件中相同的名字冲突。以同样的方式,也能把 push 和 pop 用来做栈操作的变量 sp 和 val 隐藏起来,即声明 sp 和 val 为 static。
外部的 static 声明最常用于变量,但它也能用于函数。通常,函数名是全局的,对整个程序的任何部分都可见。然而,如果一个函数被声明为 static,它的名字在它声明的文件之外是不可见的。
static 声明也能用于内部变量。内部的 static 变量对于其所在的函数是局部的,这点正如自动变量一样,但和自动变量不一样的是,即使函数没有被激活的时候(函数激活即指每次进入和离开函数),内部 static 变量也是存在的。这意味着,内部的 static 变量在一个函数内提供了私有的,永久的存储空间。
练习4-11、修改 getop,使之不需要使用 ungetch。提示:使用内部的 static 变量。