从头学C(34)外部变量

0

形容词 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

Leave A Reply