作用域是一个在绝大多数(事实上我没有见过没有作用域概念的程序语言)程序语言的必然组成部分。日常良好的使用使用习惯和程序设计风格,有时候让我们渐渐忘记了这些作用域的准确定义,因为它通常没给我们带来什么麻烦。这里我将其写下来,仅在勾起大家模糊回忆中清晰的轮廓,也许有你所知的,也许有你所惑的,又或者可能我的理解和你的理解甚至格格不入,烦请大家积极指正。
几个概念:
a.外部变量,许多程序设计的书上用“全局变量”一词,这里做一个统一,本文中“外部变量”就是所谓的“全局变量”。
b.自动变量,许多程序设计的书上用“局部变量”一词,这里做一个统一,本文中“自动变量”就是所谓的也可以称作“动态局部变量”,与此相对的还有“静态局部变量”,这两种局部变量的并集就是通常所说的“局部变量”了。
这两种叫法都是有其特定的道理和阐述的意义的。局部变量就是直言了其作用的范围,它是局部的可见的(这一点您应该深有体会)。因为在一个函数内声明一个变量,形如int a;它从它的声明式开始,直到该函数的末尾(以“}”符号所标识)有效。在函数体的外部,该变量a不存在并且无效。这样的声明事实上是auto int a;声明式的一种省略写法,因此称之为“自动变量”,因为它在该函数调用开始的时候初始化内存空间,在调用结束的时候将释放该函数的内存空间,而这一切不需要人工的干预,因此它是自动的。
c.静态变量,又称之为“静态局部变量”,它通常与auto int a;的形式相反,它采用了static关键字进行修饰,static int a;。这样的声明导致了它在编译时就分配了内存空间,并在整个程序的运行过程中持续有效。唯一的限制是它仅在它所在的函数范围内有效。
1、外部变量的作用域从它声明的位置开始,到其所在(待编译)的文件的末尾结束。
参看以下代码:
#include <stdio.h>
#include <stdlib.h>
void extern1Processor(void);
int main(void){
/*下面的语句因为extern1Variable变量的定义在main函数的后面
* 因此该函数将产生编译错误。
* extern1Variable' undeclared (first use in this function)
printf("main.extern1Variable:%d\n",extern1Variable);*/
extern1Processor();
return EXIT_SUCCESS;
}
int extern1Variable = 2003;
/*下面能够正确使用extern1Variable变量,因为extern1Variable的
* 定义在extern1Processor(void);方法之上。*/
void extern1Processor(void){
printf("extern1Processor.extern1Variable:%d\n",extern1Variable);
}
2、如果要在外部变量的定义之前使用该变量,则必须在相应的变量声明中强制地使用关键字extern。
参看以下代码:
#include <stdio.h>
#include <stdlib.h>
void extern2Processor(void);
int main(void) {
extern int extern2Variable;
printf("extern2Processor.extern2Variable:%d\n", extern2Variable);
extern2Processor();
return EXIT_SUCCESS;
}
int extern2Variable = 2004;
void extern2Processor(void) {
printf("extern2Processor.extern2Variable:%d\n", extern2Variable);
}
输出结果:
extern2Processor.extern2Variable:2004
extern2Processor.extern2Variable:2004
3、如果外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制地使用关键字extern。
假设在不同的源文件file1.c与file2.c中,我们都需要定义一个变量int aVariable = 2;时,分别编译二者,它们将都包含一个变量aVariable的声明和定义。但是当我们将它们一起加载的时候,由于它们都是外部变量,相同的变量名导致编译器不知道它们的主次关系。因此,这里我们要求程序员一定要用extern将主次分出来。比如file1.c中int aVariable = 2;这句话声明了aVariable的类型为int,为它分配了sizeof(int)内存大小的一块内存空间,它的值为2。在file2.c中我们声明它extern int aVariable;(这里不能使用extern int aVariable=3;但是可以使用int aVariable;)这句话告诉了编译器aVariable不是我这个源文件中进行声明的,它来自外部(一个未知的位置)。这样在单独编译该文件gcc -c file2.c的时候就不会因为缺失声明式而引发编译错误了。
有同学认为这里的int aVariable;是声明,不是定义,这是一种错误的观点。在外部变量中,形如:
file1.c file2.c
---------------------------------------------------------------
int aVariable = 3; int aVariable;
int main(){
……
}
其中的int aVariable;定义了外部变量aVariable,并为之分配了存储单元。
这同时也成为了外部变量和静态变量必须是常量表达式,且只初始化一次的理由。如果我们对两边都进行初始化(定义),编译器将不知道让谁成为主要初始化的值。事实上,外部变量和静态变量的值初始化过程是在编译时进行的,它们的值都被放在静态存储区,也就是我们惯常在汇编中的DATA SEGMENT部分,因此它们必须是常量表达式,并且有且只有初始化一次,否则我们将可能写出类似这样的语句(而这样的语句本身就是错误的):
DATA SETMENT
INFO1 "INFOMATION1"
"INFOMATION2" ;这样的定义是不允许的
DATA ENDS
……
按照概念extern通常被看作是外部的,因此通常情况下初始化操作一般是在无extern的声明式后的,若在extern一边进行初始化,则有违常理(主次不分了)。但是由于将初始化步骤仅放在extern一边满足只初始化一次的原则,因此编译不会出错,但是根据不同的编译器可能会给出警告提示。
参看以下代码:
包含main的源文件:
#include <stdio.h>
#include <stdlib.h>
void extern3Processor(void);
int main(void) {
extern3Processor();
return EXIT_SUCCESS;
}
extern int extern3Variable;
void extern3Processor(void) {
printf("extern3Processor.extern3Variable:%d\n", extern3Variable);
}
externFile.c文件:(这个文件很简单,就包含了一个外部变量的定义)
int extern3Variable = 2005;
编译多个文件如下:(将.c文件编译为.o文件,再将这几个.o文件一起加载到.exe文件中(UNIX中通常为.out文件),通常在对个别文件作出修改后,我们只需要重新编译那个文件,然后将这些新旧.o文件一起加载到.exe文件中即可。)
gcc -O0 -g3 -Wall -c -fmessage-length=0 -osrc\EffectiveArea.o ..\src\EffectiveArea.c
gcc -O0 -g3 -Wall -c -fmessage-length=0 -osrc\externFile.o ..\src\externFile.c
gcc -oEffectiveArea.exe src\externFile.o src\EffectiveArea.o
输出结果:
extern3Processor.extern3Variable:2005
4、之前提到的文字中包含“声明”和“定义”,它们其实有着严格的区别。
声明:变量声明用于说明变量的属性(主要是变量的类型)
定义:变量定义除了需要声明之外,还引起了存储器分配。
5、在步骤3中我们发现在任意文件中定义的外部变量均可在其它文件中进行使用,只要我们使用了extern关键字告诉编译器这个变量的声明式,我们就可以顺利通过编译。虽然这个方式能够实现在多个文件中共享数据,但是考虑到文件的管理与项目的不可预测性,这样的方式未免让我们有了些许的担心。要是我定义的变量被别人恶意引用了怎么办?对于只进行读操作的行为,可能这种灾难是比较小的,但是对于写操作的行为,就有可能影响到变量的正确性。
用static声明限定外部变量和函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。
参看以下代码:
包含main的源文件:
#include <stdio.h>
#include <stdlib.h>
void static1Processor(void);
int main(void) {
static1Processor();
return EXIT_SUCCESS;
}
extern int static1Variable;
void static1Processor(void) {
printf("static3Processor.static3Variable:%d\n", static1Variable);
}
staticFile.c文件:(这个文件很简单,就包含了一个外部变量的定义,与externFile.c所不同的是它的定义增加了static关键字修饰):
static int static1Variable = 2006;
输出结果(编译错误,无任何输出结果):
由于static int static1Variable = 2006;导致了static1Variable变量只对staticFile.c文件可见。
对于函数,它也是具有类似的限制:
包含main的源文件:
#include <stdio.h>
#include <stdlib.h>
/*两种定义均无法引用void static2Processor(void);方法的具体实现。*/
/*void static2Processor(void);*/
extern void static2Processor(void);
int main(void) {
static2Processor();
return EXIT_SUCCESS;
}
staticFile.c:
#include <stdio.h>
static int static2Variable = 2007;
static void static2Processor(void) {
printf("static2Processor.static1Variable:%d\n", static2Variable);
}
static不仅可以用于声明外部变量,它还可以用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定局部变量,只能在该函数中使用,但它与自动变量不同的是,不管其所在函数是否被调用,它将一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。换句话说,static类型的内部变量只是一种只能在某个特定函数中使用但一直占据存储空间的变量。
6、因为extern是为了防止重复定义,而不是防止重复声明。因此对于不可能产生重复定义的函数声明式来说,形如void extern4Processor(void); 这样的语句可以不用增加extern,因为它是重复的声明,而不是定义。
因为函数声明式本身是不允许嵌套的,因此它天生就是外部的,所以默认情况下类似void FunctionName(){……};的形式都有个默认的修饰符extern void FunctionName(){……};只有标识了static的函数不是外部函数。
包含main的源文件:
#include <stdio.h>
#include <stdlib.h>
/*double kinds of declare
* The storage-class specifier, if any,in the declaration specifiers
* shall be either extern or static .
*/
/*extern void extern4Processor(void);*/
void extern4Processor(void);
int main(void) {
extern4Processor();
return EXIT_SUCCESS;
}
externFile.c:
#include <stdio.h>
int extern4Variable = 2008;
void extern4Processor(void) {
printf("extern4Processor.extern4Variable:%d\n", extern4Variable);
}
输出结果:
extern4Processor.extern4Variable:2008
7、重复声明并不可怕,可怕的是重复定义。
参看以下代码:
包含main的源文件:
#include <stdio.h>
#include <stdlib.h>
char repeatVariableWithoutDefinition1; /*declear thrice no hurt*/
char repeatVariableWithoutDefinition1; /*declear thrice no hurt*/
char repeatVariableWithoutDefinition1; /*declear thrice no hurt*/
void repeatVariableWithoutDefinition1Func(void);
int main(void) {
repeatVariableWithoutDefinition1Func();
return EXIT_SUCCESS;
}
void repeatVariableWithoutDefinition1Func(void) {
repeatVariableWithoutDefinition1 = 'v';
printf("repeatVariableWithoutDefinition1:%c\n",
repeatVariableWithoutDefinition1);
}
otherFile.c:
char repeatVariableWithoutDefinition1;
char repeatVariableWithoutDefinition1;
它的无害是因为它们都是在编译时进行分配的,它们并没有并存,只是仅存了一个罢了。
8、至此,上面已经生成了许多的声明/定义。可以看出,我们的函数是可以跨文件调用的,而且每次调用都要写函数声明式。为此,C语言支持“头文件”,也就是我们经常看到的#include "xxxxx.h"或#include <stdio.h>。其中include <>的时候,将根据相应规则查找该文件(通常在编译器所在Includes文件夹内找),但是""的时候总是先在源文件(*.c)所在文件夹查找,若找不到则使用与#include <>相同规则进行查找。
#include是一个C预处理器,它所指定的文件将在编译时,将其中内容原封不动地替换到#include语句所在的位置。这样的话,我们就有能力实现了一个地方定义函数,多个地方调用的功能了。(每次重新写声明式难免会造成:1、手误,导致拼写错误;2、修改维护困难,可能会漏掉,但又机缘巧合不会出错。)
#include头文件中可以推荐包含文件声明、宏替换等(事实上可以包含任何的文本)。
因为可以包含任何的文本,所以我们有可能因为重复定义而导致一些不必要的麻烦,因为毕竟重复定义是没有任何意义的,还增加编译时间。因此在头文件的内部,我们通常采用条件包含来避免重复地包含。
参看以下代码:
#ifndef EFFECTIVEAREA_H_
#define EFFECTIVEAREA_H_
/*define the content of EffectiveArea.h here!*/
#endif /* EFFECTIVEAREA_H_ */
注意,宏名字是全局有效的,因此我们必须保证它的唯一性,否则,在判断的时候,就会因为两个头文件之间的互相排斥(被认为是同一个文件),但事实上它们之间只是错误地定义了名字。为此我们可以用文件名的等价转换来包含它们,因为文件名是唯一的。(文件名包含它的路径,通常我们将头文件放入同一个文件夹下,因此我们可以保证在同级文件夹下的文件名的唯一性。)
这样我们就可以随心所欲地包含头文件了,而不必担心重复包含头文件所带来的坏处了。
9、对于函数签名声明返回值类型为int的可以省略(不推荐(引发警告))。
参看以下代码:
#include <stdio.h>
#include <stdlib.h>
/*We can omit the declaration of function here only when it's returnType is int.
* It only cause compile warning "implicit declaration of function 'nodeclare1Func'"
* Because the default function returnType is 'int'. */
/*But we suggest you explicit declare your function here.*/
/*int nodeclare1Func(int param1); */
/*We can not omit anything here!*/
char nodeclare2Func(void);
int main(void) {
nodeclare1Func(2009);
nodeclare2Func();
return EXIT_SUCCESS;
}
int nodeclare1Func(int param1) {
printf("nodeclare1Func.param1:%d\n", param1);
return EXIT_SUCCESS;
}
char nodeclare2Func(void) {
printf("nodeclare2Func.i:%d\n", 2010);
return 'c';
}
作用域的内容并不难,掌握它们虽然不需要花费太多的时间(当然我还是比较推崇认真掌握了),但是我们应该从使用习惯上做到合理准确地应用这些特性。对于出现的一些错误能够给出合理的解释。