2. 在模块中维护内部状态
当调用一个典型的函数时,会声明一些局部变量,但是当函数返回时,这些变量值就被丢弃了。
因为必须要记录内部状态,扫描器的实现是不允许其中的函数每次返回时都丢弃所有信息的。
2.1 全局变量
在函数内部声明的变量叫局部变量,局部变量仅存在于一个栈帧中。当函数返回时,其栈帧中的变量就完全消失了。
- 全局变量(global variable)
变量声明也可以在函数定义之外出现,以这种方式声明的变量称为全局变量。
例如,在代码片段
int g;
void MyProceure()
{
int i;
...
}
中, 变量g
是个全局变量,而变量i
是局部变量。
局部变量i
仅在函数MyProceure
内有效,而全局变量g
可以在模块中随后声明的任何函数中使用。
可以使用变量的程序部分叫变量的作用域。
这样,局部变量的作用域是定义它的函数中,全局变量的作用域则是源文件中在它所出现后的其余部分。
和局部变量不同,全局变量以某一种方式保持在内存中,在这种方式下它的值不受函数调用的影响。
全局变量保持相同的值直到给它赋予新值为止。
2.2 使用全局变量的危险性
缺点:使用全局变量会使得代码很难读懂。
例如,在查找一个由于变量被错误赋值而导致的程序错误。
如果是个全局变量,由于模块中的每个函数都能操作那个变量,那么问题可能存在于源文件的任何位置;
而局部变量的错误位置则更容易确定。如果局部变量值错误,只需要查找应用该变量的函数即可。
为了避免这样的问题,在结构良好的程序中,一般很少使用全局变量。
它们的主要优点在于,在函数调用之间可以维护它们的值。
2.3 保持变量的模块私有化
全局变量在一个模块的任何地方都可见只是使用它们的问题之一。
除非明确地声明,否则C编译器假定其他模块也可以看到全局变量。
这样,当声明了一个全局变量后,可能改变其值的函数不只是局限在一个模块中。该变量可能被整个程序的任何模块引用。
在一个结构良好的程序中,独立的模块之间通过在模块间传递参数的函数调用来交换数据。
在大多情况中,最好确保每个全局变量不会被一个以上的模块引用。
为了避免两个模块引用同一个全局变量的可能性,应该在声明前用关键字static
来彻底避免这种危险。如:
static int cpos;
这个声明定义cpos
为一个全局整型变量,在所定义的模块里的任何地方都可见。
然而,cpos
对于别的模块是无效的,因此它是当前模块私有的。
在本书中,所有的全局变量都用static
来声明。
2.4 初始化全局变量
- 动态初始化(dynamic initialization)
使用一个初始化函数来给代表模块内部状态的全局变量赋值的方式称为动态初始化。
其主要特点是,在程序运行时执行。
在C语言中,也可以在程序运行前给全局变量赋初值。
- 静态初始化(static initialization)
由于这种初始化在执行程序之前发生,因而称为静态初始化。
为了定义一个全局变量的静态初始化,要在声明中的变量名之后加上一个等号,然后跟上初值,初值必须是一个常量。
例如,声明
static int startingValue=1;
不仅声明startingValue
为此模块的私有全局变量,而且保证当程序开始运行时变量的内容是1。
在本书中,大多数维护内部状态的接口使用动态初始化,并包括一个函数来显式执行它。
然而对于以下两种情况,静态初始化是更好的选择:
(1) 变量值在程序整个生命周期内都是常量。
(2) 变量的初值只有少数几个客户想要改变。
对于第二种情况,举例如下:
假设一个扫描器模块的客户要求改变扫描器接口以便所有的包含字母的记号都以大写形式返回。这样,在调用
InitScanner("Hello there");
之后,用户希望记号为"HELLO","“和"THERE”。
这个行为只对其中某一些客户会有用。
如何满足这个客户的需要,同时不会让其他客户不满意呢?
需要在从GetNextToken
返回之前调用函数ConvertToUpperCase
。
另一方面,仅当用户有此要求时才这样做。
为了追踪客户是否想要大写记号,可以声明如下所示的全局布尔变量:
static bool uppercaseFlag;
如果uppercaseFlag
为真,扫描器全部返回大写记号;如果为假,则照原样返回。
如何初始化uppercaseFlag
,如何设计接口来让客户改变这个标志的值?
这些问题引出了一些接口设计的重要问题。
一种途径是,使用动态初始化。
客户通过传递额外的布尔参数给设置uppercaseFlag
选项的InitScanner
来选择行为方式。即,调用:
InitScanner("Hello there", TRUE);
InitScanner("Hello there", FALSE);
然而,该方法存在两个严重缺点:
(1) 程序阅读者很难知道在调用InitScanner
时TRUE和FALSE参数的含义。为理解这些参数的用途,任何客户都不得不阅读接口注释。
(2) 新的设计改变了已有的接口而破坏了稳定性。如果扫描接口已经有客户,那么这些客户
不得不修改程序。
避免以上两种问题的更好的办法是,扩展扫描器接口而非改变它:
所有老的函数都像以前那样工作;
为了提供返回大写记号的选项,可以增加一个新的函数ReturnUppercaseTokens
,它带有一个布尔值,用来设置uppercaseFlag
。
因此,调用
ReturnUppercaseTokens(TURE);
客户可以选择返回大写记号的新的行为模式。
使用老的scanner.h
接口的现有程序中没有一个程序会调用ReturnUppercaseTokens
。
为了保证它有恰当的值,需要使用静态初始化:
static bool uppercaseFlag=FALSE;
- 默认值(default value)
除非客户专门采取行动去改变,否则一直在程序里使用的值称为默认值。
在一个典型的模块中,指定客户可以设置的选项的全局变量通常被静态初始化为其默认值。
客户通过调用接口提供的函数来改变这些值。
2.5 私有函数
关键字static
除了用于全局变量外,还可以用来指示某函数是某个特定模块的私有函数。
定义接口时,接口导出的函数不是私有的。接口的要点就是让这些函数可以在其他模块中调用。
在很多情况下,接口还包括一些只能在当前模块中调用的函数。
要指出某一函数是否被限制在一个特定的模块中,可以把关键字static
放在函数原型和其实现的前面。
这样使得客户无法调用这些函数,从而使接口与用户间的抽象边界更加坚固稳定。
声明函数为static
在由几个程序员参与开发的大型程序环境中也有好处。
在不同函数中,避免名字相互干扰,就可以使用static
关键字来保证他们使用的名字对于自己的模块的私有化。
如下的规则对于模块化开发来说是极好的指导:
参考
《C语言的科学和艺术》 —— 第10章 模块化开发