作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
其中,块是用一对花括号括起来的代码区域。例如,整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。
另外,虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。所以到目前为止,我们使用的局部变量(包括函数的形式参数)都具有块作用域。因此,下面代码中的变量cleo和patrick都具有块作用域:
double blocky(double cleo){ double patrick = 0.0; ... return patrick;}
声明在内层块中的变量,其作用域仅局限于该声明所在的块:
double blocky(double cleo){ double patrick = 0.0; int i; for (i = 0; i < 10; i++) { double q = cleo * i; // start of scope for q ... patrick *= q; } // end of scope for q ... return patrick;}
在该例中,q的作用域仅限于内层块,只有内层块中的代码才能访问q。
以前,具有块作用域的变量都必须声明在块的开头。C99标准放宽了这一限制,允许在块中的任意位置声明变量。因此,对于for的循环头,现在可以这样写:
for (int i = 0; i < 10; i++) printf("A C99 feature: i = %d", i);
为适应这个新特性,C99把块的概念扩展到包括for循环、while循环、do-while循环和if语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分。所以,上面for循环中的变量i被视为for循环块的一部分,它的作用域仅限于for循环。一旦程序离开for循环,就不能再访问i。
函数作用域(function-scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。
函数原型作用域(function prototype scope)用于函数原型中的形参名(变量名),如下所示:
int mighty(int mouse, double large);
函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。
而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用:
void use_a_VLA(int n, int m, ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。变量的定义在函数的外面,具有文件作用域(file-scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。考虑下面的例子:
#include int units = 0; /* a variable with file scope */void critic(void);int main(void){ ...}void critic(void){ ...}
这里,变量units具有文件作用域,main()和critic()函数都可以使用它(更准确地说,units具有外部链接文件作用域,稍后讲解)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)。
翻译单元和文件
我们常常会认为多个文件在编译器中可能以一个文件出现。例如,通常在源代码(.c扩展名)中包含一个或多个头文件(.h扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。
但是,C预处理实际上是用包含的头文件内容替换#include指令。所以,编译器把源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。