1.10外部变量和作用域
上一节main中的变量,如line、longest等等,对main来说是私有的或者说是局部的。因为它们是在main中定义的,其他函数不能直接访问它们。其他函数中的变量也是如此,例如,getline中的变量 i 与copy中的变量 i 没有关系。函数中的局部变量只在函数被调用时存在,当函数退出后就消失了。这就是为什么这样的变量通常被称作“自动”变量(遵循其他语言的术语)。我们后续也将使用“自动”这个词来指代这些局部变量。(第4章会谈到static存储类型,其中的局部变量可以在调用间保持它们的值。)
因为自动变量伴随着函数的调用或退出而出现或消失,它们不能在调用之间保持它们的值,每次进入时都必须明确地设定值。如果不设定值,就会包含垃圾值。
如果不想用自动变量的话,可以定义对所有函数来说都是“外部”的变量,也就是说,任何函数都可以用名字来访问该变量。(这个机制很像Fortan的COMMON,或者是Pascal最外层块中定义的变量。)因为外部变量是全局可访问的,因此可以用来取代参数列表,以实现函数间的数据通信。进一步来说,因为外部变量永远存在(不会随着函数的调用和退出,出现和消失),它们甚至能在设置它们的函数返回后保持它们的值。
外部变量必须被“定义”在所有函数之外,有且仅有一次;这会为它们分配存储空间。外部变量还必须在所有需要访问它的函数内“声明”;这是用于指明变量类型。声明必须是一个extern语句,或者在某些上下文时隐式声明。为了让上面的讨论更好理解,我们重写前面的计算最长行程序,并把line、longest和max作为外部变量。这要求改变所有三个函数的调用、声明和函数主体。
注意:getline已经是C标准库函数了(POSIX.1-2008.),如果要在新环境下编译运行代码,要给getline改个名字。
#include <stdio.h>
#define MAX_LINE 1000 /*最长的行长度*/
int max; /* 目前的最大长度 */
char line[MAX_LINE]; /* 当前输入行 */
char longest[MAX_LINE]; /* 最长行存这里 */
int getline(void);
void copy(void);
/* 输出最长输入行,特别版 */
main()
{
int len;
extern int max;
extern char longest[];
max = 0;
while ((len = getline()) > 0)
if (len > max) {
max = len;
copy();
}
if (max > 0)
printf("%s", longest);
return 0;
}
/* getline: 特别版 */
int getline(void)
{
int c, i;
extern char line[];
for (i = 0; i < MAX_LINE-1 && (c = getchar())!=EOF && c != '\n'; ++i)
line[i] = c;
if (c == '\n') {
line[i] = c;
++i;
}
line[i] = '\0';
return i;
}
/* 拷贝,特别版 */
void copy(void)
{
int i;
extern char line[], longest[];
i = 0;
while ((longest[i] = line[i]) != '\0')
++i;
}
main、getline、copy中的外部变量在代码的最前几行定义,这指明了它们的类型并给它们分配了存储空间。从语法上来说,外部变量的定义和局部变量的定义是一样的,但由于它们在函数外面出现,故变量就是外部的。在函数使用外部变量之前,必须要让函数知道它的名字。一种方法是在函数内写一个extern声明;声明与前面的写法一致,区别在于前面加了个extern关键字。
在某些环境下,extern声明可以省略。如果在源文件中,外部变量的定义先于它在某个函数中的使用,那就没有必要在这个函数中加入extern声明。这样main、getline和copy中的extern声明其实是冗余的。实际上,通常的实践方式是把所有外部变量的定义放在源文件开头,就能去掉所有的extern声明。
如果程序有几个源文件,且一个变量在文件1中定义,并在文件2和文件3中使用,那么在文件2和文件3中就需要extern声明,以便关联这个变量的出现。通常的实践是把所有的外部变量和函数的声明放在一个单独的文件中(历史上被称为header头文件),在每个源文件的开头用#include来包含它。按惯例使用.h作为头文件名的后缀。例如,标准库里的函数在如<stdio.h>之类的头文件里面声明。这个主题的详细讨论会在第四章,而标准库会在第七章和附录B。
由于特别版的getline和copy没有参数,故从逻辑上看它们的原型应当是getline() 和 copy()。但为了和旧的C程序兼容,C标准将空参数列表当作是旧风格的声明,并会为其关闭参数列表校验;空参数列表必须明确地用void来表示。第四章会进一步讨论。
注意要区分“定义”和“声明”。“定义”是变量被创建并分配存储的地方。“声明”是指明变量的性质的地方,并不分配存储。
顺带一提,有人倾向是把所有能看到的东西都变成extern变量,因为这看起来能简化通信——参数列表很短,而且只要你想,你总是能访问这些变量。但问题在于,即使你不想要的时候,外部变量也总是存在着。过于依赖外部变量是非常危险的,因为它导致程序的数据关联完全不明显——变量可能因意外甚至疏忽被改变,程序也很难修改。第二版的最长行程序比第一版差,部分是由于这个原因,而部分是因为破坏了两个有用函数的通用性(把它们和它们操作的变量绑定起来了)。
本章到这里我们已经覆盖了C语言的常规核心部分。用这些基础部分,可能写成长度相当可观的、有用的程序,建议你在往下学之前试试。后面的练习题包含了比本章前面样例更复杂的程序。
(第一章完)