Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 15 из 43)

C++ имеет небольшой, но гибкий набор различных видов операторов

для контроля потока управления в программе и богатый набор операций

для манипуляции данными. С наиболее общепринятыми средствами вас

познакомит один законченный пример. После него приводится

резюмирующий обзор выражений и с довольно подробно описываются

явное описание типа и работа со свободной памятью. Потом

представлена краткая сводка операций, а в конце обсуждаются стиль

выравнивания* и комментарии.

3.1 Настольный калькулятор

С операторами и выражениями вас познакомит приведенная здесь

программа настольного калькулятора, предоставляющего четыре

стандартные арифметические опреации над числами с плавающей точкой.

Пользователь может также определять переменные. Например, если

вводится

r=2.5

area=pi*r*r

(pi определено заранее), то программа калькулятора напишет:

2.5

19.635

где 2.5 - результат первой введенной строки, а 19.635 - результат

второй.

Калькулятор состоит из четырех основных частей: программы

синтаксического разбора (parser'а), функции ввода, таблицы имен и

управляющей программы (драйвера). Фактически, это миниатюрный

компилятор, в котором программа синтаксического разбора производит

синтаксический анализ, функция ввода осуществляет ввод и

лексический анализ, в таблице имен хранится долговременная

информация, а драйвер распоряжается инициализцией, выводом и

обработкой ошибок. Можно было бы многое добавить в этот

калькулятор, чтобы сделать его более полезным, но в существующем

виде эта программа и так достаточно длинна (200 строк), и большая

часть дополнительных возможностей просто увеличит текст программы

не давая дополнительного понимания применения C++.

____________________

* Нам неизвестен русскоязычный термин, эквивалентный английскому

indentation. Иногда это называется отступами. (прим. перев.)

- стр 78 -

3.1.1 Программа синтаксического разбора

Вот грамматика языка, допускаемого калькулятором:

program:

END // END - это конец ввода

expr_list END

expr_list:

expression PRINT // PRINT - это или '\n' или ';'

expression PRINT expr_list

expression:

expression + term

expression - term

term

term:

term / primary

term * primary

primary

primary:

NUMBER // число с плавающей точкой в C++

NAME // имя C++ за исключением '_'

NAME = expression

- primary

( expression )

Другими словами, программа есть последовательность строк. Каждая

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

Основными элементами выражения являются числа, имена и операции *,

/, +, - (унарный и бинарный) и =. Имена не обязательно должны

описываться до использования.

Используемый метод синтаксического анализа обычно называется

рекурсивным спуском; это популярный и простой нисходящий метод. В

таком языке, как 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 -

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

посмотреть, осуществляется ли присваивание ему, или оно просто

используется. В обоих случаях надо справиться в таблице имен. Сама