形容词 external 和 internal 相对,internal 用于描述定义在函数内部的函数参数及变量,外部变量则定义在函数之外,因此可以在多个函数中使用。
第四章 函数与程序结构 >> 4.3 外部变量
1. 性质及用途
C语言不允许在函数中定义其他函数,可以认为函数都是“外部的”。那么,外部变量和函数具有相同的性质:通过同一个名字对外部变量的引用实际上都是引用了同一个对象(标准中把这一性质称为外部链接)。
由于这个性质,在不同函数之间,可以利用外部变量进行数据交换,而不是通过调用函数时传递参数来实现。显然,当需要交换的数据比较复杂时,直接使用外部变量要比传递一长串参数更方便、有效。(之前的章节 从头学C(11)外部变量与作用域 中也提到过,这种方法会导致函数间数据联系太多、程序不容易维护,所以这种方法也需要谨慎一些)
外部变量的另一个好处是:它比内部变量具有更大的作用域和更长的生存期。我们已经知道,自动变量只能在函数内部使用(从其所在的函数被调用时变量开始存在,在函数退出时变量消失),而外部变量是永久存在的(它们的值在一次函数调用到下一次函数调用之间保持不变,这也是为什么外部变量可以作为共享数据,给不同函数使用的原因)。
2. 逆波兰表示法
在逆波兰表示法中,所有运算符都跟在操作数后面。比如:
中缀表达式:(1 - 2) * (4 + 5) 逆波兰表示法: 1 2 - 4 5 + *
逆波兰表示法是不需要括号的,只需知道每个操作符需要几个操作数就不会出现意外。
接下来我们就来编写一个采用逆波兰表示法,具有加减乘除四则运算的简单计算器。
书本中的程序使用到了栈的概念。计算器程序的流程大致是:当操作数到达时,压入栈中;当运算符到达时,从栈中弹出相应数量的操作数(比如二元运算符需要用到两个操作数,就需要依次弹出两个操作数),然后进行运算,将结果再压入栈中;依此类推……;当到达输入行的末尾时,说明计算已完成,把栈顶的值弹出并打印,便是最终结果。
程序的设计如下:
while(下一个运算符或操作数不是文件结束符EOF) if(是操作数) 将该数压入栈中 else if(是运算符) 弹出所需数目的操作数 计算结果 将结果压入栈中 else if(是换行符) 弹出并打印栈顶的值 else 出错
从流程分析,栈的弹出和压入操作我们可以设计成独立的函数,另外还需要一个读取下一个运算符或操作数的独立函数。
对 main 来说,它并不关心栈的具体细节(比如栈存放在哪?),只需要知道数据可以push进去,再pop出来即可。因此可以把栈及相关信息放在外部变量中,只供 push 和 pop 函数访问,而不提供给 main 函数访问。
这里我们把该程序分割成了 4 个源文件。依次是
main.c
#include <stdio.h> #include <stdlib.h> /* for atof() */ #define MAXOP 100 /* max size of operand or operator */ #define NUMBER '0' /* signal that a number was found */ extern int getop(char []); extern void push(double); extern double pop(void); /* reverse Polish calculator */ int main() { int type; double op2; char s[MAXOP]; while ((type = getop(s)) != EOF) { switch (type) { case NUMBER: push(atof(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("error: unknown command %s\n", s); break; } } return 0; }
对于加法和乘法,操作数的弹出顺序无关紧要;但对于减法和除法,运算符左右操作数必须要注意。因此上面的程序是先将右操作数弹出到一个临时变量中,再弹出左操作数进行运算,而不是采用类似下面的这种做法:
push(pop() - pop()); /* 错误 */
因为在上面这个函数调用中,并没有定义两个 pop 调用的求值次序。
pop_push.c
#include <stdio.h> #define MAXVAL 100 /* maximum depth of val stack */ int sp = 0; /* next free stack position */ double val[MAXVAL]; /* value stack */ /* push: push f onto value stack */ void push(double f) { if (sp < MAXVAL) val[sp++] = f; else printf("error: stack full, can't push %g\n", f); } /* pop: pop and return top value from stack */ double pop(void) { if (sp > 0) return val[--sp]; else { printf("error: stack empty\n"); return 0.0; } }
push 和 pop 函数都要用到共享的栈和栈顶指针,因此这两个变量放在了函数外部,定义成外部变量。而同时main 函数并不关心栈的引用以及栈顶指针 sp 的信息,因此将栈的细节放在这个源文件中,对 main 函数而言,它们是隐藏的(我们会在下一节中分析这个)。
getop.c
#include <stdio.h> #include <ctype.h> #define NUMBER '0' /* signal that a number was found */ extern int getch(void); extern int ungetch(int); /* getop: get next character or numeric operand */ int getop(char s[]) { int i, c; while ((s[0] = c = getch()) == ' ' || c == '\t') ; s[1] = '\0'; if (!isdigit(c) && c!= '.') return c; /* not a number */ 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.c
#include <stdio.h> #define BUFSIZE 100 char buf[BUFSIZE]; /* buffer for ungetch */ int bufp = 0; /* next free position in buf */ /* get a (possibly pushed-back) character */ int getch(void) { return (bufp > 0) ? buf[--bufp] : getchar(); } /* push character back on input */ void ungetch(int c) { if (bufp >= BUFSIZE) printf("ungetch: too many characters\n"); else buf[bufp++] = c; }
getch 和 ungetch 函数主要是因为程序不能确定一个操作数是否读完(比如 1.2 需要读 3 个字符),因此需要预先读取下一个字符来判断是否读取完整了。而正是由于预先读取了下一个字符,会导致下一轮读取操作数会出现异常,因此需要将这个预先读取的下一个字符再放回原来的缓冲区中,以供下一轮的读取操作。
由于缓冲区和下标变量是供 getch 和 ungetch 共享的,所以它们也是被定义成了外部变量。
最后,编译命令如下:
gcc main.c pop_push.c getop.c getch_ungetch.c
执行编译后的程序,并输入上面的逆波兰表达式,可以得到下面的结果:
1 2 - 4 5 + * -9