Главная · Поиск книг · Поступления книг · Top 40 · Форумы · Ссылки · Читатели

Настройка текста
Перенос строк


    Прохождения игр    
Demon's Souls |#15| Dragon God
Demon's Souls |#14| Flamelurker
Demon's Souls |#13| Storm King
Demon's Souls |#12| Old Monk & Old Hero

Другие игры...


liveinternet.ru: показано число просмотров за 24 часа, посетителей за 24 часа и за сегодня
Rambler's Top100
Образование - Страустрап Б. Весь текст 579.17 Kb

Язык С++

Предыдущая страница Следующая страница
1 ... 8 9 10 11 12 13 14  15 16 17 18 19 20 21 ... 50
рекурсивным спуском;  это популярный  и простой нисходящий метод. В
таком языке,  как C++,    в  котором  вызовы  функций  относительно
дешевы, этот  метод к  тому же  и эффективен.  Для каждого  правила
вывода  грамматики  имеется  функция,  вызывающая  другие  функции.
Терминальные символы   (например,  END, NUMBER, + и -) распознаются
лексическим  анализатором  get_token(),  а  нетерминальные  символы
распознаются функциями  синтаксического анализа  expr(),  term()  и
prim().  Как  только  оба  операнда  (под)выражения  известны,  оно
вычисляется; в  настоящем компиляторе  в  этой  точке  производится
генерация кода.
  Программа  разбора   для  получения   ввода  использует   функцию
get_token(). Значение  последнего вызова  get_token()  находится  в
переменной curr_tok;  curr_tok имеет  одно из значений перечисления
token_value:

  enum token_value {
      NAME        NUMBER    END
      PLUS='+'    MINUS='-'    MUL='*'    DIV='/'
      PRINT=';'    ASSIGN='='    LP='('    RP=')'
  };
  token_value curr_tok;

                             - стр 79 -

  В каждой  функции разбора  предполагается, что  было обращение  к
get_token(), и  в curr_tok  находится очередной  символ, подлежащий
анализу.  Это  позволяет  программе  разбора  заглядывать  на  один
лексический символ  (лексему) вперед  и заставляет  функцию разбора
всегда читать  на одну  лексему больше,  чем используется правилом,
для обработки  которого она  была вызвана.  Каждая функция  разбора
вычисляет "свое"  выражение и  возвращает значение.  Функция expr()
обрабатывает сложение  и вычитание;  она состоит из простого цикла,
который ищет термы для сложения или вычитания:

  double expr()                   // складывает  и вычитает
  {
      double left = term();

      for(;;)                     // ``навсегда``
          switch(curr_tok) {
          case PLUS:
              get_token();        // ест '+'
              left += term();
              break;
          case MINUS:
              get_token();        // ест '-'
              left -= term();
              break;
          default:
              return left;
          }
  }

Фактически сама функция делает не очень много. В манере, достаточно
типичной для  функций более  высокого уровня  в больших программах,
она вызывает  для выполнения  работы другие  функции. Заметьте, что
выражение 2-3+4 вычисляется как (2-3)+4, как указано грамматикой.
  Странная  запись   for(;;)  -   это  стандартный   способ  задать
бесконечный  цикл;  можно  произносить  это  как  "навсегда"*.  Это
вырожденная  форма   оператора  for;   альтернатива   -   while(1).
Выполнение оператора  switch повторяется  до тех пор, пока не будет
найдено ни  + ни  -, и  тогда выполняется  оператор return в случае
default.
  Операции +=  и  -=  используются  для  осуществления  сложения  и
вычитания. Можно  было бы  не изменяя смысла программы использовать
left=left+term() и  left=left-term(). Однако  left+=term() и  left-
=term()  не   только  короче,   но  к   тому   же   явно   выражают
подразумеваемое действие.  Для бинарной  операции @  выражение x@=y
означает x=x@y  за исключением  того, что x вычисляется только один
раз. Это применимо к бинарным операциям

  +    -    *    /    %    &    |    ^    <<    >>

поэтому возможны следующие операции присваивания:

  +=    -=    *=    /=    %=    &=    |=    ^=    <<=    >>=

____________________
  * игра слов: "for" - "forever" (навсегда). (прим. перев.)

                             - стр 80 -

  Каждая  является  отдельной  лексемой,  поэтому  a+  =1  является
синтаксической ошибкой  из-за пробела  между +  и  =.  (%  является
операцией взятия  по модулю;  &,| и ^ являются побитовми операциями
И, ИЛИ  и исключающее  ИЛИ; <<  и >>  являются операциями  левого и
правого сдвига).  Функции term()  и get_token() должны быть описаны
до expr().
  Как организовать  программу в  виде набора  файлов, обсуждается в
Главе 4.  За одним  исключением все  описания  в  данной  программе
настольного  калькулятора   можно  упорядочить   так,   чтобы   все
описывалось ровно один раз и до использования. Исключением является
expr(), которая  обращается к  term(), которая обращается к prim(),
которая в  свою очередь  обращается к expr(). Этот круг надо как-то
разорвать; описание

  double expr();    // без этого нельзя

перед prim() прекрасно справляется с этим.
  Функция  term()  аналогичным  образом  обрабатывает  умножение  и
сложение:

  double term()                    // умножает и складывает
  {
      double left = prim();

      for(;;)
          switch(curr_tok)    {
          case MUL:
              get_token();         // ест '*'
              left *= prim();
              break;
          case DIV:
              get_token();         // ест '/'
              double d = prim();
              if (d == 0) return error("деление на 0");
              left /= d;
              break;
          default:
              return left;
          }
  }

  Проверка, которая  делается, чтобы  удостовериться в том, что нет
деления на  ноль, необходима,  поскольку результат  деления на ноль
неопределен и  как правило  является роковым.  Функция error(char*)
будет описана позже. Переменная d вводится в программе там, где она
нужна, и сразу же инициализируется. Во многих языках описание может
располагаться  только   в  голове   блока.  Это  ограничение  может
приводить к  довольно скверному  искажению  стиля  программирования
и/или излишним  ошибкам. Чаще всего неинициализированнные локальные
переменные являются  просто признаком  плохого  стиля;  исключением
являются переменные,  подлежащие инициализации посредством ввода, и
переменные векторного  или структурного  типа, кторые нельзя удобно

                             - стр 81 -

инициализировать одними  присваиваниями*. Заметьте,  что = является
операцией присваивания, а == операцией сравнения.
  Функция prim,  обрабатывающая primary,  написана в основном в том
же духе, не считая того, что немного реальной работы в ней все-таки
выполняется, и  нет нужды  в цикле,  поскольку мы попадаем на более
низкий уровень иерархии вызовов:

  double prim()                 // обрабатывает primary (первичные)
  {
      switch (curr_tok) {
      case NUMBER:              // константа с плавающей точкой
          get_token();
          return number_value;
      case NAME:
          if (get_token() == ASSIGN) {
              name* n = insert(name_string);
              get_token();
              n->value = expr();
              return n->value;
          }
          return look(name-string)->value;
      case MINUS:               // унарный минус
          get_token();
          return -prim();
      case LP:
          get_token();
          double e = expr();
          if (curr_tok != RP) return error("должна быть )");
          get_token();
          return e;
      case END:
          return 1;
      default:
          return error("должно быть primary");
      }
  }

  При обнаружении  NUMBER (то  есть, константы с плавающей точкой),
возвращается  его  значение.  Функция  ввода  get_token()  помещает
значение в  глобальную  переменную  number_value.  Использование  в
программе  глобальных   переменных  часто   указывает  на  то,  что
структура не  совсем прозрачна,  что  применялась  некоторого  рода
оптимизация.  Здесь   дело   обстоит   именно   так.   Теоретически
лексический  символ   обычно  состоит  из  двух  частей:  значения,
определяющего вид лексемы (в данной программе token_value), и (если
необходимо) значения  лексемы. У  нас имеется  только одна  простая
переменная  curr_tok,  поэтому  для  хранения  значения  последнего
считанного NUMBER  понадобилась глобальная переменная number_value.
Это  работает   только  потому,  что  калькулятор  при  вычислениях
использует только одно число перед чтением со входа другого.
  Так же,  как значение  последнего встреченного  NUMBER хранится в
number_value, в  name_string   в виде  символьной  строки  хранится
представление последнего прочитанного NAME. Перед тем, как что-либо
____________________
  * В  языке немного  лучше этого с этими исключениями тоже надо бы
справляться. (прим. автора)

                             - стр 82 -

сделать  с  именем,  калькулятор  должен  заглянуть  вперед,  чтобы
посмотреть, осуществляется  ли присваивание  ему,  или  оно  просто
используется. В  обоих случаях надо справиться в таблице имен. Сама
таблица описывается  в #3.1.3;  здесь надо  знать только,  что  она
состоит из элементов вида:

  srtuct name {
      char* string;
      char* next;
      double value;
  }

где next используется только функциями, которые поддерживают работу
с таблицей:

  name* look(char*);
  name* insert(char*);

  Обе возвращают  указатель на  name, соответствующее  параметру  -
символьной строке;  look() выражает  недовольство, если имя не было
определено. Это  значит, что  в калькуляторе можно использовать имя
без  предварительного   описания,  но   первый   раз   оно   должно
использоваться в левой части присваивания.

     3.1.2 Функция ввода

  Чтение ввода  - часто самая запутанная часть программы. Причина в
том, что  если программа должна общаться с человеком, то она должна
справляться с  его  причудами,  условностями  и  внешне  случайными
ошибками. Попытки  заставить человека  вести себя более удобным для
машины  образом   часто   (и   справедливо)   рассматриваются   как
оскорбительные. Задача  низкоуровненовой программы  ввода состоит в
том, чтобы читать символы по одному и составлять из них лексические
символы более  высокого уровня. Далее эти лексемы служат вводом для
программ  более   высокого  уровня.   У  нас  ввод  низкого  уровня
осуществляется get_token(). Обнадеживает то, что написание программ
ввода низкого  уровня не  является ежедневной  работой;  в  хорошей
системе для этого будут стандартные функции.
  Для калькулятора  правила ввода  сознательно были выбраны такими,
чтобы функциям  по работе  с потоками  было  неудобно  эти  правила
обрабатывать; незначительные изменения в определении лексем сделали
бы get_token() обманчиво простой.
  Первая сложность  состоит в  том, что  символ новой  строки  '\n'
является для калькулятора существенным, а функции работы с потоками
считают его  символом пропуска.  То есть,  для  этих  функций  '\n'
значим только  как ограничитель лексемы. Чтобы преодолеть это, надо
проверять пропуски (пробел, символы табуляции и т.п.):

  char ch

  do {    // пропускает пропуски за исключением '\n'
      if(!cin.get(ch)) return curr_tok = END;
  } while (ch!='\n' && isspace(ch));

                             - стр 83 -

  Вызов cin.get(ch)  считывает один  символ из  стандартного потока
ввода в ch. Проверка if(!cin.get(ch)) не проходит в случае, если из
cin нельзя  считать ниодного  символа; в  этом случае  возвращается
END,  чтобы   завершить  сеанс  работы  калькулятора.  Используется
операция  !  (НЕ),  поскольку  get()  возвращает  в  случае  успеха
ненулевое значение.
  Функция (inline)  isspace() из  обеспечивает стандартную
проверку на  то, является  ли символ пропуском (#8.4.1); isspace(c)
возвращает ненулевое значение, если c является символом пропуска, и
ноль в  противном случае.  Проверка реализуется  в  виде  поиска  в
таблице,  поэтому  использование  isspace()  намного  быстрее,  чем
проверка на  отдельные символы  пропуска;  это  же  относится  и  к
функциям isalpha(),  isdigit() и  isalnum(), которые используются в
get_token().
  После  того,   как  пустое   место  пропущено,   следущий  символ
используется для  определения того, какого вида какого вида лексема
приходит. Давайте  сначала рассмотрим  некоторые  случаи  отдельно,
прежде чем  приводить всю  функцию. Ограничители  лексем '\n' и ';'
Предыдущая страница Следующая страница
1 ... 8 9 10 11 12 13 14  15 16 17 18 19 20 21 ... 50
Ваша оценка:
Комментарий:
  Подпись:
(Чтобы комментарии всегда подписывались Вашим именем, можете зарегистрироваться в Клубе читателей)
  Сайт:
 
Комментарии (4)

Реклама