C++ - Страустрап Бьярн
Шрифт:
Интервал:
Закладка:
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.
– * игра слов: «for» – «forever» (навсегда). (прим. перев.)
Операции +=, -= используются для осуществления сложения и вычитания. Можно было бы не изменяя смысла программы использовать left=left+term() и left=left-term(). Однако left+= term() и left-=term() не только короче, но к тому же явно выражают подразумеваемое действие. Для бинарной операции @ выражение [email protected]=y означает [email protected] за исключением того, что x вычисляется только один раз. Это применимо к бинарным операциям
+ – * / % amp; ! ^ «„ “»
поэтому возможны следующие операции присваивания:
+= -= *= /= %= amp;= != ^= «„= “»=
Каждая является отдельной лексемой, поэтому a+ =1 является синтаксической ошибкой из-за пробела между + и =. (% является операцией взятия по модулю; amp;,! и ^ являются побитвыми операциями И, ИЛИ и исключающее ИЛИ; «„ и “» являются операциями левого и правого сдвига). Функции 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 ввдится в программе там, где она нужна, и сразу же инициализруется. Во многих языках описание может располагаться только в голове блока. Это ограничение может приводить к довольно скверному искажению стиля программирования и/или излишним ошибкам. Чаще всего неинициализированные локальные переменные являются просто признаком плохого стиля; исключением являются переменные, подлежащие инициализации посредством ввода, и пременные векторного или структурного типа, которые нельзя удобно инициализировать одними присваиваниями*. Заметьте, что = является операцией присваивания, а == операцией сравнения.
– * В языке немного лучше этого с этими исключениями тоже надо бы справляться. (прим. автора)
Функция 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. Перед тем, как что-либо сделать с именем, калькулятор должен заглнуть вперед, чтобы посмотреть, осуществляется ли присваивание ему, или оно просто используется. В обоих случаях надо спрвиться в таблице имен. Сама таблица описывается в #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' amp; amp; isspace(ch));
Вызов cin.get(ch) считывает один символ из стандартного потока ввода в ch. Проверка if(!cin.get(ch)) не проходит в случае, если из cin нельзя считать ни одного символа. В этом случае возвращается END, чтобы завершить сеанс работы кальклятора. Используется операция ! (НЕ), поскольку get() возврщает в случае успеха ненулевое значение.