netlib.narod.ru< Назад | Оглавление | Далее >

4.3. Внешние переменные

Программа на Си обычно оперирует с множеством внешних объктов: переменных и функций. Прилагательное «внешний» (external) противоположно прилагательному «внутренний», которое относится к аргументам и переменным, определяемым внутри функций. Внешние переменные определяются вне функций и потенциально доступны для многих функций. Сами функции всегда являются внешними объектами, поскольку в Си запрещено определять функции внутри других функций. По умолчанию одинаковые внешние имена, даже в компилируемых отдельно файлах, относятся к одной и той же внешней функции или переменной. (В стандарте это называется редактированием внешних связей (external linkage).) В этом смысле внешние переменные похожи на области COMMON в Фортране и на переменные самого внешнего блока в Паскале. Позже мы покажем, как можно сделать внешние функции и переменные видимыми только внутри одного исходного файла.

Поскольку внешние переменные доступны всюду, их можно использовать как альтернативу аргументам и возвращаемым значениям для связи между функциями. Любая функция может обратиться по имени к объявленной где-либо в программе внешней переменной.

Если число совместно используемых функциями переменных велико, внешние переменные могут оказаться более удобными и эффективными, чем длинные списки аргументов. Но, как отмечалось в главе 1, к этому заявлению следует относиться критически, поскольку такая практика ухудшает структуру программы и приводит к слишком большому числу связей между функциями по данным.

Внешние переменные полезны благодаря большой области видимости и продолжительному времени жизни. Автоматические переменные существуют только внутри функции, они возникают в момент входа в функцию и исчезают при выходе из нее. Внешние переменные, напротив, существуют постоянно, так что их значения сохраняются и между обращениями к функциям. Таким образом, если двум функциям приходится пользоваться одними и теми же данными и ни одна из них не вызывает другую, то часто бывает удобно оформить эти общие данные в виде внешних переменных, а не передавать их в функцию и обратно через аргументы.

Давайте исследуем тему на примере. Пусть необходимо написать программу-калькулятор, понимающую операторы +, -, * и /. Чтобы упростить реализацию мы будем использовать обратную польскую нотацию вместо инфиксной. (Обратная польская нотация применяется в некоторых карманных калькуляторах и в таких языках, как Forth и Postscript.)

В обратной польской записи оператор следует за своими операндами. Выражение в инфиксной записи, скажем

    (1 - 2) * (4 + 5)

в польской записи представляется как

    1 2 - 4 5 + *

Скобки не нужны, неоднозначности в вычислениях не бывает, поскольку известно, сколько операндов требуется для каждого оператора.

Реализовать нашу программу весьма просто. Каждый операнд помещается в стек; если встречается оператор, то из стека берется соответствующее число операндов (два для бинарных операторов), выполняется операция, после чего результат помещается в стек. В нашем примере числа 1 и 2 помещаются в стек, а затем замещаются на их разность — –1. Далее в стек помещаются числа 4 и 5, которые затем заменяются их суммой — 9. Числа –1 и 9 заменяются в стеке их произведением (т.е. –9). Встретив символ новой строки, программа извлекает значение из стека и печатает его.

Таким образом, программа состоит из цикла, обрабатывающего на каждом своем шаге очередной встречаемый оператор или операнд:

    while (следующий символ не EOF)
      if (число)
        записать число в стек
      else if (оператор)
        взять из стека операнды
        выполнить операцию
        поместить результат в стек
      else if (новая строка)
        взять число из стека и напечатать
      else
        ошибка

Операции «записать в стек» и «взять из стека» сами по себе тривиальны, однако по мере добавления к ним механизмов обнаружения и нейтрализации ошибок становятся достаточно длинными. Поэтому их лучше оформить в виде отдельных функций, а не повторять соответствующий код по всей программе. кроме того, необходима отдельная функция для получения очередного оператора или операнда.

Главный вопрос, который мы еще не рассмотрели, — где расположить стек и каким функциям разрешить к нему прямой доступ. Стек можно расположить в функции main и передавать сам стек и текущую позицию в нем в качестве аргументов функциям, которые записывают значения в стек и получают значения из стека. Но функции main нет дела до переменных, относящихся к стеку, — ей нужны только операции по помещению чисел в стек и извлечению их оттуда. Поэтому мы решили стек и связанную с ним информацию хранить во внешних переменных, доступных для функций push и pop, но недоступных для main.

Переход от эскиза к программе достаточно легок. Если теперь программу представить как текст, расположенный в одном исходном файле, она будет иметь следующий вид:

    #include  /* могут быть в любом количестве */
    #define   /* могут быть в любом количестве */

    объявления функций для main

    main() { ... }

    внешние переменные для push и pop

    void push (double f) { ... }
    double pop (void) { ... }
    
    int getop(char s[]) { ... }

    подпрограммы, вызываемые функцией getop

Позже мы обсудим, как текст этой программы можно разместить в нескольких исходных файлах.

Функция main — это цикл, содержащий инструкцию switch, передающую управление на ту или иную ветвь в зависимости от типа оператора или операнда. Здесь представлен более типичный случай применения инструкции switch по сравнению с рассмотренным в параграфе 3.4.

#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(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(" Ошибка: деление на нуль\n");
                break;
            case '\n':
                printf("\t%.8g\n", pop());
                break;
            default:
                printf("Ошибка: неизвестная операция %s\n", s);
                break;
        }
    }
    return 0;
}

Для операторов + и * порядок, в котором операнды берутся из стека, не важен, однако в случае операторов - и /, порядок извлечения операндов влияет на результат. Таким образом запись

    push(pop() - pop());  /* НЕПРАВИЛЬНО */

является неправильной, так как очередность обращения к pop не определена. Чтобы гарантировать правильную очередность, необходимо первое значение из стека присвоить временной переменной, как это сделано в main.

Посмотрим теперь, как в программе реализуется стек.

#define MAXVAL 100      /* максимальная глубина стека */

int sp = 0;             /* следующая свободная позиция в стеке */
double val[MAXVAL];     /* стек */

/* push: положить значение f в стек */
void push(double f)
{
    if (sp < MAXVAL)
        val[sp++] = f;
    else
        printf("Ошибка: стек полон, %g не помещается\n", f);
}

/* pop: взять число из стека и вернуть в качестве результата */
double pop(void)
{
    if (sp > 0)
        return val[--sp];
    else {
        printf("Ошибка: стек пуст\n");
        return 0.0;
    }
}

Переменная считается внешней, если она определена вне функции. Таким образом, стек и индекс стека, которые совместно используются функциями push и pop, определяются вне этих функций. Но main не использует ни стек, ни позицию в стеке, и поэтому их представление может быть скрыто от main.

Займемся реализацией getop — функции, получающей следующий оператор или операнд. Задача проста: пропускаем пробелы и табуляции; если следующий символ не цифра и не десятичная точка, то нужно вернуть его; в противном случае надо получить строку цифр (возможно с десятичной точкой) и в качестве результата вернуть константу 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? Во многих случаях программа не может определить, прочла ли она все необходимые данные, пока не прочтет лишнюю информацию. Так, ввод составляющих число символов производится до тех пор, пока не встретится символ, отличный от цифры. Но это означает, что программа прочла на один символ больше, чем нужно, и последний символ нельзя включать в число.

Эту проблему можно было бы решить с помощью операции обратной чтению символа, которая отменяла бы чтение ненужного символа. Тогда каждый раз, когда программа считает на один символ больше, чем требуется, эта операция возвращала бы его во входной поток, и остальная часть программы могла бы вести себя так, будто этот символ вовсе не был считан. К счастью, описанный механизм обратной посылки символа легко моделируется с помощью пары работающих совместно функций. Функция getch поставляет очередной символ из входного потока, а ungetch отправляет символ назад во входной поток, так что при следующем обращении к getch мы вновь его получим.

Нетрудно догадаться, как они работают вместе. Функция ungetch запоминает посылаемый назад символ в некотором буфере, представляющем собой массив символов, доступный для обеих функций; getch читает из буфера, если там что-то есть, или обращается к getchar, если буфер пустой. Следует также предусмотреть индекс, указывающий на положение текущего символа в буфере.

Так как функции getch и ungetch совместно используют буфер и индекс, значения последних должны сохраняться между вызовами. Поэтому буфер и индекс должны быть внешними по отношению к этим функциям, и мы можем записать getch, ungetch и общие для них переменные в следующем виде:

#define BUFSIZE 100

char buf[BUFSIZE];  /* буфер для ungetch */
int bufp = 0;       /* номер свободной позиции в буфере */

int getch(void)     /* получить символ из потока ввода */
{
  return (bufp > 0) ? buf[--bufp] : getchar();
}

void ungetch(int c) /* вернуть символ в поток ввода */
{
  if (bufp >= BUFSIZE)
    printf("ungetch: слишком много символов\n");
  else
    buf[bufp++] = c;
}

Стандартная библиотека включает функцию ungetc, обеспечивающую возврат одного символа (см. главу 7). Мы же, чтобы проиллюстрировать более общий подход, для запоминания возвращаемых символов использовали массив.


Упражнение 4-3


Исходя из предложенной нами схемы, дополните программу-калькулятор таким образом, чтобы она «понимала» оператор получения остатка от деления (%) и отрицательные числа.



Упражнение 4-4


Добавьте команды, с помощью которых можно было бы печатать верхний элемент стека (с сохранением его в стеке), дублировать его в стеке, менять местами два верхних элемента стека. Введите команду очистки стека.



Упражнение 4-5


Предусмотрите возможность использования в программе библиотечных функций sin, exp и pow. См. библиотеку <math.h> в приложении В (параграф 4).



Упражнение 4-6


Введите команды для работы с переменными (легко обеспечить до 26 переменных, каждая из которых имеет имя, представленное одной буквой латинского алфавита). Добавьте переменную, предназначенную для хранения самого последнего из напечатанных значений.



Упражнение 4-7


Напишите программу ungets(s), возвращающую строку s во входной поток. Должна ли ungets «знать» что-либо о переменных buf и bufp, или ей достаточно пользоваться только функцией ungetch?



Упражнение 4-8


Предположим, что число символов, возвращаемых назад, не превышает одного. Модифицируйте с учетом этого факта функции getch и ungetch.



Упражнение 4-9


В наших функциях не предусмотрена возможность возврата EOF. Подумайте, что надо сделать, чтобы можно было возвращать EOF, и скорректируйте соответственно программу.



Упражнение 4-10


В основу программы калькулятора можно положить применение функции getline, которая читает целиком строку; при этом отпадет необходимость в getch и ungetch. Напишите программу, реализующую этот подход.



netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz