13.6 使用外部变量
13.6.1 外部变量
1.局部变量的生存期间和有效区间
迄今为止,本书代码中变量定义的位置都是在函数内部(包括形参)。从代码的空间角度来看,这些变量也仅仅在所在函数的内部或所在的块的内部使用。本书称这种变量为“局部变量”。如果是“auto”类别的局部变量,变量是从程序执行到变量定义处开始存在,程序执行到离开变量定义所在的最内层的块时该变量消失。如果是“static”类别的局部变量,这些变量从程序开始执行时就存在,程序结束时消失,但是只能在其所在的块内才能使用这些变量。如图13-6所示,显示了局部变量的生存期间和有效区间的范围。
图13-6 局部变量的有效范围和存在时间
2.外部变量的有效区间和生存期间
在C语言中,变量也可以定义在(所有)函数外部,这种变量叫做外部变量(External Variables)。外部变量的生存期间是从程序运行开始到程序结束。如果外部变量在定义时如果没有被初始化,那么其初值是这个变量被赋值为0的结果。
外部变量的有效区间分为两种情况:static存储类别的外部变量的有效范围是从变量定义的位置到变量定义所在的文件结束处;extern存储类别的外部变量的有效范围可以是整个源程序(包括构成源程序中的其他源文件)。
3.static存储类别的外部变量
由于static存储类别的外部变量的有效范围为定义处开始到定义所在源文件的结尾,所以在其后面的各个函数定义中都可以对这个变量进行修改。毫无疑问,这破坏了结构化程序设计的原则:各个部分之间的联系越弱越好。这种变量实际上把后面定义的各个函数紧密地“连接”了一个整体,后面几个函数可能以这个数据为公共的中心开展工作而不是“各自为战”、“各个击破”。
但是这种变量也有其优势,那就是这时不用再通过函数参数传递数据了。如果后面的各个函数都是关于这个static外部变量的操作,也不是绝对不可以使用static外部变量。至于其利弊,只有具体问题具体分析了。
定义static存储类别外部变量的方法是在定义变量时在前面加上static关键字,另外注意需要把它定义在函数定义的外部,举例如下。
这里定义的“int”类型的“i”就是一个static存储类别的外部变量,在f2()和f3()中都可以使用。如果希望在f1()中使用这个外部变量,在使用之前需要有变量“说明”,如下所示。
这个说明的含义是告诉编译器这个“i”是个“int”类型的外部变量,是在程序的其他位置定义的。
变量“定义”与变量“声明”的含义和形式都很相近,但却有一个巨大的差别,变量“定义”意味着要求编译器为变量安排内存空间,而变量“说明”不涉及为变量安排内存的问题。变量定义时可以进行初始化,变量声明时不可以进行初始化。“定义”和“说明”的相同之处在于它们都是在描述某个标识符的数据类型。
顺便说一句,在“说明”数组名标识符时可以忽略“[]”中的数组尺寸数据。例如
有些C语言的书津津乐道地讨论为什么不可以把“extern int a[];”写成“extern int *a;”,笔者看不出这种讨论有什么意义。因为如果一向很规矩地把“说明”写成“extern int a[3];”压根就不可能遇到这种问题。
总之,static存储类别的外部变量的有效范围可以是所在的整个源文件。C语言规定,其他文件不可以使用不在本文件内定义的static存储类别的外部变量。
4.extern存储类别的外部变量
extern存储类别的外部变量在源程序范围内都可以使用。
extern是外部变量的缺省(默认)存储类别。换句话说,在定义extern类别的外部变量时,通常不需要在变量前面加“extern”这个关键字。只要没有用static定义外部变量,那么它就是extern类别的。
但如果是在其他文件中或者在本文件中extern类别外部变量定义位置之前使用这个外部变量,需要对这个外部变量进行“说明”。“变量说明”和“变量定义”在形式上很相似,但前者不涉及为变量开辟内存空间,只说明标识符的性质,而后者则不但说明标识符的性质,编译器还必须为这个标识符开辟内存空间。
由于“变量说明”和“变量定义”在形式上很相似,这甚至可能发生混淆,在定义extern外部变量时,建议初始化以明确地向编译器和代码阅读者表明是“定义”。
说明某个标识符是一个在别处定义的外部变量时,需要使用“extern”关键字。例如
其含义是说明“i”是一个在别处定义了的外部变量。在说明一个外部变量时不要进行初始化。
下面代码说明了extern外部变量的定义和使用方法。
如果使用源程序中非本文件中定义的extern类别的外部变量,那么在使用之前必须要进行变量说明。
不难看出,extern类别的外部变量的作用范围更大,因此在本质上更加背离结构化程序设计的原则。一般来说,除几种很特殊的情况外,使用外部变量都是两害相权取其轻的选择。
5.外部变量的使用场合
因而,除非迫不得已,否则应该尽量不使用外部变量,尽管使用外部变量可以带来减少函数调用的参数这样的“好处”。
有些情况下使用外部变量是无奈之举,比如在数据较大、局部变量无法存储时。
在C语言中,各个函数内的auto局部变量所占的总内存空间的大小是有限制的,一旦所要定义的变量超过限制范围,能进行的选择就只有全局变量或局部静态变量。尽管局部静态变量不影响结构化程序设计原则,但可能带来函数参数过多或尺寸过大、程序效率降低等问题。
此外,使用动态分配内存时也可能存在内存分配不成功的可能,这在内存资源比较少的情况下可能是一个很突出的问题。这时,选择外部变量可能是一个比较恰当的解决方案。
一旦选择外部变量作为数据存储方案,应该时刻警惕它所带来的种种弊端。并且,只要有可能,应该特别限制外部变量的作用范围,这个范围应该越小越好。也就是说,如果必须使用外部变量,应该尽量选择static存储类别。但是很可惜,C语言没有把“static”作为外部变量的默认存储类别。因为如此,很多人把C语言以extern作为外部变量的默认存储类别看成是C语言的一个重大缺点。
13.6.2 外部变量的应用
1.变量定义的位置
仅仅是为了演示和体会外部变量的使用方法,本例题选择用外部变量作为具象棋盘的存储方案。必须声明的是,这未必是很好的一个解决方案,笔者是在很无奈的心情下并充分考虑到程序中只需要一个“具象棋盘”的现实才这样做的。
现在接着考虑“13.5.4显示空的棋盘”之外其余的部分。
在“4_具象棋盘.h”中定义了“具象棋盘”这种数据类型之后,需要考虑的问题就是在何处定义这种类型的变量。既然已经决定把这个程序中唯一的具象棋盘定义为外部变量,那么把这个变量与对这个变量进行直接操作的函数定义在同一个模块中通常应该是较为恰当的选择。这样在main()中进行函数调用时也免去了传递参数的麻烦。
基于这些考虑,在工程中添加“4_具象棋盘行为.c”文件,在其中定义外部变量。
static具象棋盘类型 具象棋盘;
定义为static存储类别是希望这个外部变量的作用尽量局部化,同时也要求其他对这个棋盘进行直接操作的函数也被定义在这个模块中。
如果对这个棋盘进行操作的函数都定义在这个模块中,那么显然这些函数都不必传递这个参数,而且由于源程序内只有这唯一一个具象的棋盘,显然,“4具象棋盘.h”文件中没有必要提供这种数据类型的定义,应该把这个数据类型的定义移到“4具象棋盘行为.c”中。当然“4具象棋盘.h”这个文件本身还是必要的,因为它还需要为其他模块提供函数原型。这样“4具象棋盘行为.c”文件的内容就如下所示。
程序代码13-27(4_具象棋盘行为.c)
由于main()中的“显示(空的)棋盘”和“显示棋盘(的变化)”明显功能一致,都是把同一个“具象棋盘”数据对象(只是不同时刻)的内容输出,所以这里把它们合并成了一个——“显示棋盘()”。但是这里存在一个问题,即“显示的棋盘”这个数据对象的初始化问题。事实上,“显示(空的)棋盘”这个调用有一个潜在的隐含的要求,即棋盘需要被清空。如果不打算通过一个函数解决这个问题,那么只好用变量初始化的方法。
2.棋盘的设计与初始化
从“显示的棋盘”的数据类型来看,显然可以用下面的方式初始化。
1 这个末尾的“,”没有任何问题,数组初始化时容许最后一项后面有“,”,结构体也如此。
但是把设计棋盘的工作与设计代码的工作放在一起进行,多半不是明智之举。最好把这个设计工作放在代码外面进行。具体可以在工程所在的文件夹(目录)建立一个文件,假如取名为“棋盘界面.TXT”,由于这是个文本文件,所以很容易设计出适于GUI界面表现的、比较理想的界面,而且它易于修改。可以使用IDE、Windows下的“记事本”程序或其他文本编辑器来方便地编辑这个文件。而在“4_具象棋盘行为.c”文件中只需要:
就可以了。
这是#include预处理命令的二种很让人意外的用法,但这不是很正规的做法。在使用这种方法时需要注意,预处理命令需要单独写成一行,此外“棋盘界面.TXT”这个文件不可以放在工程中,因为这个文件不是用C语言或预处理命令写成的。
需要特别说一句,把界面设计与代码设计分开是程序设计的一个普遍原则。从软件工程的角度看,把两者搅在一起是很糟糕的工作作风。
在完成了“具象棋盘”类型设计、变量定义、初始化及“显示棋盘()”函数后(目前尚只有一个空函数,但完成这个函数已经不存在本质的困难了),就可以回到最初的模块继续编程了。