MyMSDN

MyMSDN记录开发新知道

C语言作用域、声明以及定义

作用域是一个在绝大多数(事实上我没有见过没有作用域概念的程序语言)程序语言的必然组成部分。日常良好的使用使用习惯和程序设计风格,有时候让我们渐渐忘记了这些作用域的准确定义,因为它通常没给我们带来什么麻烦。这里我将其写下来,仅在勾起大家模糊回忆中清晰的轮廓,也许有你所知的,也许有你所惑的,又或者可能我的理解和你的理解甚至格格不入,烦请大家积极指正。

几个概念:

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';
}

作用域的内容并不难,掌握它们虽然不需要花费太多的时间(当然我还是比较推崇认真掌握了),但是我们应该从使用习惯上做到合理准确地应用这些特性。对于出现的一些错误能够给出合理的解释。

posted on 2008-08-19 20:42 volnet 阅读(5441) 评论(13)  编辑 收藏 引用 所属分类: C/C++

评论

# re: C语言作用域、声明以及定义[未登录] 2008-08-20 09:53 raof01

1、……到其所在*编译单元*的结束。如果“到其所在(待编译)的文件的末尾结束”,那头文件中定义的外部变量就会……基本没有作用。

2、extern表示被声明的variable或者函数在其他地方定义并分配存储空间了。如果使用了extern,就有可能不必include相应的头文件。

3、“如果外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制地使用关键字extern。”——声明/定义变量时,无论是全局还是局部变量,不带extern永远是定义。

“通常在对个别文件作出修改后,我们只需要重新编译那个文件”——这更多的是模块间的依赖问题。

“这一点给人的感觉static非常像C++/C#/Java中的private修饰符,即仅对当前文件(其它语言中可能并非文件)有效。”——你该好好理解一下OO了。static在C++中一种意思就是与C一样。C#有assembly的概念,其internal更像static,但又不完全像。Java我不熟,不过也绝不是你说的这样。private是访问权限修饰符(access permission modifier),而static是存储说明(storage class specifier),两者是*完全*不同的概念。——强烈要求你仔细阅读相关书籍以免误导大家。

char repeatVariableWithoutDefinition1; /*declear thrice no hurt*/——真的no hurt吗?

强调一点:编译单元和源文件是有区别的。  回复  更多评论   

# re: C语言作用域、声明以及定义 2008-08-20 11:21 volnet

@raof01
感谢非常细心的点评
对于private的部分,我可没划上等号呢,我只能说它们有部分形式上的相同,但绝不相等。。。。我说的当前文件,其间不一定是文件的概念,这个可能要意会了~~~  回复  更多评论   

# re: C语言作用域、声明以及定义[未登录] 2008-08-20 17:38 raof01

@volnet
两个风牛马不相及的东西被你说成相似——熊猫和小熊猫。

感觉你自己的理解还是比较混乱。
  回复  更多评论   

# re: C语言作用域、声明以及定义 2008-08-20 21:33 volnet

@raof01
他们在可见性方面有些许感性上的相似吧
至于其它方面,本来就是两个不同的概念,怎么可能说完全一致呢~呵呵

我还是把它删了吧,哈,理解一个概念本来没必要从另一个概念入手的,当时顺手一写酿成大祸  回复  更多评论   

# re: C语言作用域、声明以及定义[未登录] 2008-08-21 08:56 raof01

@volnet
互相学习,互相促进而已。你到我的博客文章里去找联系方式吧,哈哈
  回复  更多评论   

# re: C语言作用域、声明以及定义 2008-08-21 09:56 volnet

@raof01
用Gtalk加你了,不知道是否正确,还是用Hotmail?  回复  更多评论   

# re: C语言作用域、声明以及定义[未登录] 2008-08-21 13:06 raof01

@volnet
用mail吧,不习惯聊天  回复  更多评论   

# re: C语言作用域、声明以及定义 2008-08-21 23:49 volnet

@raof01
呵呵,ok  回复  更多评论   

# re: C语言作用域、声明以及定义 2008-08-24 16:04 dell

如果外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制地使用关键字extern。忽略它很会导致问题,让人困惑。  回复  更多评论   

# re: C语言作用域、声明以及定义[未登录] 2008-08-26 09:00 raof01

@dell
会导致什么问题?怎么让人困惑?说话别不明不白的  回复  更多评论   

# re: C语言作用域、声明以及定义[未登录] 2008-08-28 14:49 raof01

参考这里:http://www.cppblog.com/jinq0123/
还有这里:http://blog.chinaunix.net/u/12783/showart_548200.html  回复  更多评论   

# re: C语言作用域、声明以及定义 2009-10-16 22:05 你好!

能否把我给你的评论(最前面的两条中有)删掉,我的留言中使用了邮件地址,收到了无数的垃圾邮件,烦都烦死了。如果可以,真的不胜感激。  回复  更多评论   

# re: C语言作用域、声明以及定义 2009-10-17 12:09 volnet

@你好!
已删除!  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


特殊功能