Читать интересную книгу Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 74 75 76 77 78 79 80 81 82 ... 337
Первичное выражение

  + Первичное выражение

Число:

  литерал_с_плавающей_точкой

Ввод из потока cin через поток Token_stream с именем ts.

*/

Здесь мы использовали блок комментариев, который начинается символами /* и заканчивается символами */. В реальной программе история пересмотра может содержать сведения о том, какие именно изменения были внесены и какие улучшения были сделаны. Обратите внимание на то, что эти комментарии помещены за пределами кода. Фактически это несколько упрощенная грамматика: сравните правило для Инструкции с тем, что на самом деле происходит в программе (например, взгляните на код в следующем разделе). Этот комментарий ничего не говорит от цикле в функции calculate(), позволяющем выполнять несколько вычислений в рамках одного сеанса работы программы. Мы вернемся к этой проблеме в разделе 7.8.1.

7.7. Исправление ошибок

Почему мы прекращаем работу программы, когда находим ошибку? В свое время это казалось простым и очевидным решением, но почему? Почему бы не вывести сообщение об ошибке и продолжить работу? Помимо всего прочего, мы часто делаем опечатки, и такие ошибки не означают, что мы решили не выполнять вычисления. Итак, попробуем исправить ошибки. Это по существу значит, что мы должны перехватить исключение и продолжить работу после исправления ошибки.

До сих пор все ошибки представлялись в виде исключений и обрабатывались функцией main(). Если мы хотим исправить ошибку, то функция calculate() должна перехватывать исключения и попытаться устранить неисправность прежде, чем приступить к вычислению следующего выражения.

void calculate()

{

  while (cin)

  try {

    cout << prompt;

    Token t = ts.get();

    while (t.kind == print) t=ts.get(); // сначала

                                        // игнорируем все

                                        // инструкции "печать"

    if (t.kind == quit) return;

    ts.putback(t);

    cout << result << expression() << endl;

  }

  catch (exception& e) {

    cerr << e.what() << endl; // выводим сообщение об ошибке

    clean_up_mess();

  }

}

Мы просто поместили цикл while в блоке try, который выводит сообщения об ошибке и устраняет неисправности. После этого работу можно продолжать по-прежнему. Что означает выражение “устранить неисправность”? В принципе готовность к выполнению вычислений после исправления ошибки означает, что все данные находятся в полном порядке и вполне предсказуемы. В калькуляторе единственные данные за пределами отдельных функций находятся в потоке Token_stream. Следовательно, мы должны убедиться, что в потоке нет лексем, связанных с прекращенными вычислениями и способных помешать будущим вычислениям.

Рассмотрим пример.

1++2*3; 4+5;

Эти выражения вызывают ошибку, и лексемы 2*3; 4+5 останутся в буферах потоков Token_stream и cin после того, как второй символ + породит исключение.

У нас есть две возможности.

1. Удалить все лексемы из потока Token_stream.

2. Удалить из потока все лексемы Token_stream, связанные с текущими вычислениями.

В первом случае отбрасываем все лексемы (включая 4+5;), а во втором — отбрасываем только лексему 2*3, оставляя лексему 4+5 для последующего вычисления. Один выбор является разумным, а второй может удивить пользователя. Обе альтернативы одинаково просто реализуются. Мы предпочли второй вариант, поскольку его проще протестировать. Он выглядит проще. Чтение лексем выполняется функцией get(), поэтому можно написать функцию clean_up_mess(), имеющую примерно такой вид:

void clean_up_mess() // наивно

{

  while (true) { // пропускаем,

                 // пока не обнаружим инструкцию "печать"

    Token t = ts.get();

    if (t.kind == print) return;

  }

}

К сожалению, эта функция не всегда работает хорошо. Почему? Рассмотрим следующий вариант:

1@z; 1+3;

Символ @ приводит нас к разделу catch в цикле while. Тогда для выявления следующей точки с запятой вызываем функцию clean_up_mess(). Функция clean_up_mess() вызывает функцию get() и считывает символ z. Это порождает следующую ошибку (поскольку символ z не является лексемой), и мы снова оказываемся в блоке catch внутри функции main() и выходим из программы. Ой! У нас теперь нет шансов вычислить лексему 1+3. Вернитесь к меловой доске!

Можно было бы уточнить содержание блоков try и catch, но это внесет в программу еще большую путаницу. Ошибки в принципе трудно обрабатывать, а ошибки, возникающие при обработке других ошибок, обрабатывать еще труднее. Поэтому стоит попытаться найти способ удалять из потока Token_stream символы, которые могут породить исключение. Единственный путь для ввода данных в калькулятор пролегает через функцию get(), и он может, как мы только что выяснили, порождать исключения. Таким образом, необходима новая операция. Очевидно, что ее целесообразно поместить в класс Token_stream.

class Token_stream {

public:

  Token_stream(); // создает поток Token_stream, считывающий

                  // данные из потока cin

  Token get();    // считывает лексему

  void putback(Token t); // возвращает лексему

  void ignore(char c);   // отбрасывает символы,

                         // предшествующие символу с включительно

private:

  bool full;             // есть лексема в буфере?

  Token buffer; // здесь хранится лексема, которая возвращается

                // назад с помощью функции putback()

};

Функция ignore() должна быть членом класса Token_stream, так как она должна иметь доступ к его буферу. Мы выбрали в качестве искомого символа аргумент функции ignore(). Помимо всего прочего, объект класса Token_stream не обязан знать, что калькулятор считает хорошим символом для исправления ошибок. Мы решили, что этот аргумент должен быть символом, потому что не хотим рисковать, работая с составными лексемами (мы уже видели, что при этом происходит). Итак, мы получаем следующую функцию:

void Token_stream::ignore(char c)

  // символ c обозначает разновидность лексем

{

  // сначала проверяем буфер:

  if (full && c==buffer.kind) {

    full = false;

    return;

  }

  full = false;

  // теперь проверяем входные данные:

  char ch = 0;

  while (cin>>ch)

    if (ch==c) return;

}

В этом коде сначала происходит проверка буфера. Если в буфере есть символ c, прекращаем работу, отбрасывая этот символ

1 ... 74 75 76 77 78 79 80 81 82 ... 337
На этом сайте Вы можете читать книги онлайн бесплатно русская версия Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп.
Книги, аналогичгные Программирование. Принципы и практика использования C++ Исправленное издание - Бьёрн Страуструп

Оставить комментарий