class="p1">Этот вариант немного сложнее: мы ввели цикл для поиска символов
+ и
–. Кроме того, дважды повторили проверку символов
+ и
–, а также дважды вызвали функцию get_token(). Поскольку это запутывает логику кода, просто продублируем проверку символов
+ и
–.
double expression()
{
double left = term(); // считываем и вычисляем Терм
Token t = get_token(); // получаем следующую лексему
while(true) {
switch(t.kind) {
case '+':
left += term(); // вычисляем Терм и добавляем его
t = get_token();
break;
case '–':
left –= term(); // вычисляем Терм и вычитаем его
t = get_token();
break;
default:
return left; // финал: символов + и – нет;
// возвращаем ответ
}
}
}
Обратите внимание на то, что — за исключением цикла — этот вариант напоминает первый (см. раздел 6.5.2.1). Мы просто удалили вызов функции expression() в функции expression() и заменили его циклом. Другими словами, перевели Выражение в грамматическом правиле в цикл поиска Терма, за которым следует символ + или –.
6.5.3. Термы
Грамматическое правило для Терма очень похоже на правило для Выражения.
Терм:
Первичное выражение
Терм '*' Первичное выражение
Терм '/' Первичное выражение
Терм '%' Первичное выражение
Следовательно, программный код также должен быть похож на код для Выражения. Вот как выглядит его первый вариант:
double term()
{
double left = primary();
Token t = get_token();
while(true) {
switch (t.kind) {
case '*':
left *= primary();
t = get_token();
break;
case '/':
left /= primary();
t = get_token();
break;
case '%':
left %= primary();
t = get_token();
break;
default:
return left;
}
}
}
К сожалению, этот код не компилируется: операция вычисления остатка (%) для чисел с плавающей точкой не определена. Компилятор вежливо предупредит нас об этом. Когда мы утвердительно ответили на вопрос 5 из раздела 6.3.5 — “Следует ли позволить ввод чисел с плавающей точкой?”, — мы не думали о таких последствиях и просто поддались искушению добавить в программу дополнительные возможности. Вот так всегда! Что же делать? Можно во время выполнения программы проверить, являются ли оба операнда операции % целыми числами, и сообщить об ошибке, если это не так. А можно просто исключить операцию % из возможностей нашего калькулятора. Эту функцию всегда можно добавить позднее (см. раздел 7.5). Исключив операцию %, получим вполне работоспособную функцию: термы правильно распознаются и вычисляются. Однако опытный программист заметит нежелательную деталь, которая делает функцию term() неприемлемой. Что произойдет, если ввести выражение 2/0? На нуль делить нельзя. Если попытаться это сделать, то аппаратное обеспечение компьютера обнаружит это и прекратит выполнение программы, выдав сообщение об ошибке. Неопытный программист обязательно столкнется с этой проблемой. По этой причине лучше провести проверку и выдать подходящее сообщение.
double term()
{
double left = primary();
Token t = get_token();
while(true) {
switch (t.kind) {
case '*':
left *= primary();
t = get_token();
break;
case '/':
{ double d = primary();
if (d == 0) error("деление на нуль");
left /= d;
t = get_token();
break;
}
default:
return left;
}
}
}
Почему мы поместили обработку операции / внутри блока? На этом настоял компилятор. Если мы хотим определить и инициализировать переменные в операторе switch, то должны поместить ее в блоке.
6.5.4. Первичные выражения
Грамматическое правило для первичных выражений также простое.
Первичное выражение:
Число
'('Выражение')'
Программный код, реализующий это правило, немного сложен, поэтому он открывает больше возможностей для синтаксических ошибок.
double primary()
{
Token t = get_token();
switch (t.kind) {
case '(': // обработка варианта '('выражение')'
{ double d = expression();
t = get_token();
if (t.kind != ')') error("')' expected");
return d;
}
case '8': // используем '8' для представления числа
return t.value; // возвращаем значение числа
default:
error("ожидается первичное выражение");
}
}
По сравнению с функциями expression() и term() в этом программном коде нет ничего нового. В нем используются те же самые языковые конструкции и методы, и объекты класса Token обрабатываются точно так же.
6.6. Испытание первой версии
Для того чтобы выполнить эти функции калькулятора, необходимо реализовать функции get_token() и main(). Функция main() тривиальна: мы просто вызываем функцию expression() и выводим результат на печать.
int main()
try {
while (cin)
cout << expression() << 'n';
keep_window_open();
}
catch (exception& e) {
cerr << e.what() << endl;
keep_window_open ();
return 1;
}
catch (...) {
cerr << "exception n";
keep_window_open ();
return 2;
}
Обработка ошибок представляет собой обычный шаблон (см. раздел 5.6.3). Отложим реализацию функции get_token() до раздела 6.8 и протестируем эту первую версию калькулятора.
ПОПРОБУЙТЕ
Первая версия программы, имитирующей работу калькулятора (включая функцию get_token()), содержится в файле calculator00.cpp. Запустите его и испытайте.
Нет ничего удивительного в том, что эта первая версия калькулятора работает не совсем так, как мы ожидали. Мы пожимаем плечами и спрашиваем себя: “Почему?”, или “Почему программа работает так,