C语言学习第二十七天,今天继续学习函数。
4.3 外部变量
C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数。外部变量定义在函数之外,因此可以在许多函数中使用。由于C语言不允许在一个函数中定义其他函数,因此函数本身是“外部的”。默认情况下,外部变量与函数具有下列性质:通过同一个名字对外部变量的所由引用(即是这种引用来自于单独编译的不同函数)实际上都是引用同一个对象(标准中把这一性质称为外部链接)。
因为外部变量可以在全局范围内访问,这就为函数之间的数据交换提供了一种可以代替函数参数与返回值的方式。任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式进行声明。
如果函数之间需要共享大量的变量,使用外部变量要比使用一个很长的参数表更方便、有效。但是,这样做必须非常谨慎,因为这种方式可能对程序结构产生不良的影响,而且可能会导致程序中各个函数之间具有太多的数据联系。
外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期。自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。而外部变量时永久存在的,它们的值在一次函数调用到下一次函数调用之间保持不变。因此,如果两个函数必须共享某些数据,而这两个函数互不调用对方,这种情况下最方便的方式便是把这些共享数据定义为外部变量,而不是作为函数参数传递。
下面我们通过一个更符再的例子来说明这一点。我们的目标是编写一个具有加、减、乘、除四则运算功能的计算器程序。为了更容易实现,我们在计算器中使用逆波兰表示法代替普通的中缀表示法。
在逆波兰表示法中,所由运算符都跟在操作数的后面。比如,下列中缀表达式:
(1 - 2) * (4 + 5)
采用逆波兰表示法表示为:
1 2 - 4 5 + *
逆波兰表示法中不需要括号,只要知道每个运算符需要几个操作数就不会引起歧义。
计算器程序的实现很简单。每个操作数都被依次压入到栈中;当一个运算符到达时,从栈中弹出相应数目的操作数(对二元运算符来说是两个操作数),把该运算符作用于弹出的操作数,并把运算结果压入到栈中。例如,对上面的逆波兰表达式来说,首先把1和2压入到栈中,再用两者之差-1取代它们;然后,将4和5压入到栈中,再用两者之和9取代它们。最后从栈中取出栈顶的-1和9,并把它们的积压入到栈顶。到达输入行的末尾时,把栈顶的值弹出并打印。
这样,该程序的结构就构成一个循环,每次循环对一个运算符及相应的操作数执行一次操作:
while (下一个运算符或操作数不是文件结束指示符)
if (是数)
将该数压入到栈中
else if (是运算符)
弹出所需数目的操作数
执行运算
将结果压入到栈中
else if (是换行符)
弹出并打印栈顶的值
else
出错
一个重要的问题:把栈放在那儿?也就是说,那些例程可以直接访问它?一种可能是把它放在主函数mian中,把栈及其当前位置作为参数传递给对它执压入或弹出操作的函数。但是,main函数不需要了解控制栈的变量信息,它只进行压入与弹出操作。因此,可以把栈及相关信息放在外部变量中,并只供push与pop函数访问,而不能被main函数访问。
把上面这段话转换成代码很容易。如果把该程序放在一个源文件中,程序可能类似与下列形式:
#include... /* 一些包含头文件 */
#define... /* 一些define定义 */
main使用的函数声明
main() { ... }
push与pop所使用的外部变量
void push(double f) { ... }
double pop(void) { ... }
int getop(char s[]) {...}
被getop调用的函数
main函数包括一个很大的switch循环,该循环根据运算符或操作数的类型控制程序的转移。
#include <stdio.h>
#include <stdlib.h> /* 为了使用atof()函数 */
#define MAXOP 100 /* 操作数或运算符的最大长度 */
#define NUMBER '0' /* 标识到一个数 */
int getop(char []);
void push (double);
double pop(void);
/* 逆波兰计算器 */
main() {
int type;
double op2;
char s[MAXOP];
while ((type = getop(s)) != EOF) {
switch (type) {
case NUMBER:
push(aotf(s));
break;
case '+':
push(pop() + pop());
break;
case '*':
push(pop() * pop());
break;
case '-':
op2 = pop();
push(pop() - op2);
break;
case '/':
op2 = pop();
if (op2 != 0.0)
push(pop() / op2);
else
printf("error: zero divisor\n");
break;
case '\n':
printf("\t%.8g\n", pop());
break;
default:
printf("eorro: unknow command %s\n", s);
break;
}
}
return 0;
}
因为+与*两个运算符满足交换率,因此,操作数的弹出次序无关紧要。但是,-与/两个运算符的左右操作数必须加以区分。在函数调用
push(pop() - pop()); /* 错 */
中并没有定义两个pop调用的求值次序。为了保证正确的次序,必须相main函数中一样把第一个值弹出到一个临时变量中。
#define MAXVAL 100 /* 栈val的最大深度 */
int sp = 0; /* 下一个空闲栈位置 */
double val[MAXVAL]; /* 值栈*/
/* push函数:把f压入到值栈中 */
void push(double f) {
if(sp < MAXVAL)
val[sp++] = f;
else
printf("error: stack full, can't push %g\n", f);
}
/* pop函数:弹出并返回栈顶的值 */
double pop(void) {
if (sp > 0)
return val[--sp];
else {
printf("error: stack empty\n");
return 0.0;
}
}
如果变量定义在任何函数的外部,则是外部变量。因此,把push和pop函数必须共享的栈和栈顶指针定义在这两个函数的外部。但是,main函数本身并没有引用栈或栈顶指针,因此对main函数而言要将它们隐藏起来。
gettop函数的实现。该函数获取下一个运算符或操作数。该任务实现起来比较容易。它需要跳过空格与制表符。如果下一个字符不是数字或小数点,则返回;否则,把这些数字字符串收集起来(其中可能包含小数点),并返回NUMBER,此标识数已经收集起来了。
#include <ctype.h>
int getch(void);
void ungetch(int);
/* getop函数:获取下一个运算符或数值操作符 */
int getop(char s[]) {
int i, c;
while ((s[0] = c = getch()) == ' ' || c == '\t')
;
s[1] = '\0';
if (!isdigit(c) && c != '.')
return c; /*不是数*/
i = 0;
if (isdigit(c)) /* 收集整数部分 */
while (isdigit(s[++i] = c = getch()))
;
if (c == '.') /* 收集小数部分 */
while (isdigit(s[++i] = c = getch()))
;
s[i] = '\0';
if (c != EOF)
ungetch(c);
return NUMBER;
}
这段函数中的getch与ungetch两个函数有什么用途呢?程序中经常会出现这样的情况:程序不能确定它已经读入的输入是否足够i,除非超前多读如一些输入u。读入一些字符以合成一个数字的情况便是一例:在看到第一个非数字字符之前,已经读入的数的完整性是不能确定的。由于程序要超前读入一个字符,这样就导致最后有一个字符不属于当前所要读入的数。
如果能“反读”不需要的字符,该问题就可以得到解决。每当程序多读入一个字符时,就把它压回到输入中,对代码其余部分而言就好像没有读入字符一样。我们可以编写一对互相协作的函数来比较方便地模拟反取字符操作。getch函数用于读入下一个待处理的字符,而ungetch函数则用于把字符放回到那个字符。
这俩个函数之间的协同工作也很简单。ungetch函数把要压回的字符放到一个共享缓冲区(字符数组)中,当该缓冲区不空时,getch函数就从缓冲区中读取字符;当缓冲取为空时,getch函数调用getchar函数直接从输入中读字符。这里还需要增加一个下标变量来记住缓冲区中当前字符的位置。
由于缓冲区与下标变量时供getch与ungetch函数共享的,且在两次调用之间必须保持值不变,因此它们必须是这两个函数的外部变量。如下:
#define BUFSIZE 100
char buf[BUFSIZE]; /* 用于ungetchar函数的缓冲区 */
int bufp = 0; /* buf中 下一个空闲位置 */
/* 取一个字符(可能是压回的字符)*/
int getch(void) {
return (bufp > 0) ? buf[--bufp] : getchar();
}
/* 把字符压回到输入中 */
void ungetch(int c) {
if (bufp > BUFSIZE)
printf("ungetch: too many characters\n");
else
buf[bufp++] = c;
}