                              Глава 3 Выражения и Операторы
           
                                                            С другой  стороны,
                                       мы не можем игнорировать эффективность
                                                                - Джон Бентли
           
             С++ имеет небольшой,  но гибкий набор различных видов  операторов
           для контроля потока управления в программе и богатый набор операций
           для манипуляции данными.  С наиболее общепринятыми  средствами  вас
           познакомит один законченный пример. После него приводится резюмиру-
           ющий обзор выражений и с довольно подробно описываются явное описа-
           ние типа и работа со свободной памятью.  Потом представлена краткая
           сводка операций,  а в конце обсуждаются стиль выравнивания* и  ком-
           ментарии.
             * Нам неизвестен русскоязычный термин, эквивалентный английскому
           indentation. Иногда это называется отступами. (прим. перев.)
                3.1 Настольный калькулятор
           
             С операторами и  выражениями  вас  познакомит  приведенная  здесь
           программа  настольного калькулятора,  предоставляющего четыре стан-
           дартные арифметические операции над  числами  с  плавающей  точкой.
           Пользователь может также определять переменные. Например, если вво-
           дится
           
             r=2.5 area=pi*r*r
           
           (pi определено заранее), то программа калькулятора напишет:
           
             2.5
             19.635
           
           где 2.5  - результат первой введенной строки,  а 19.635 - результат
           второй.
           
             Калькулятор состоит из четырех основных  частей:  программы  син-
           таксического разбора (parser'а),  функции ввода, таблицы имен и уп-
           равляющей программы (драйвера).  Фактически, это миниатюрный компи-
           лятор,  в котором программа синтаксического разбора производит син-
           таксический анализ,  функция ввода осуществляет ввод и  лексический
           анализ,  в таблице имен хранится долговременная информация, а драй-
           вер распоряжается инициализацией, выводом и обработкой ошибок. Мож-
           но  было  бы многое добавить в этот калькулятор,  чтобы сделать его
           более полезным,  но в существующем виде эта программа и так  доста-
           точно  длинна  (200 строк),  и большая часть дополнительных возмож-
           ностей просто увеличит текст программы не давая дополнительного по-
           нимания применения С++.
                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 // число с плавающей точкой в С++ NAME // имя  С++  за
                 исключением '_' NAME = expression - primary ( expression )
           
             Другими словами,  программа есть последовательность строк. Каждая
           строка состоит из одного или более выражений,  разделенных запятой.
           Основными элементами выражения являются числа,  имена и операции *,
           /,  +,  - (унарный и бинарный) и =.  Имена  не  обязательно  должны
           описываться до использования.
           
             Используемый метод  обычно называется рекурсивным спуском это по-
           пулярный и простой нисходящий метод.  В таком языке, как С++, в ко-
           тором  вызовы  функций относительно дешевы,  этот метод к тому же и
           эффективен.  Для каждого правила вывода грамматики имеется функция,
           вызывающая  другие  функции.  Терминальные символы (например,  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;
           
             В каждой  функции  разбора  предполагается,  что было обращение к
           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.
           
           ────────────────────
           * игра слов: "for" - "forever" (навсегда). (прим. перев.)
             Операции +=, -= используются для осуществления сложения и вычита-
           ния.  Можно  было  бы  не  изменяя  смысла  программы  использовать
           left=left+term()   и   left=left-term().   Однако   left+=term()  и
           left-=term() не только короче,  но к тому же явно выражают подразу-
           меваемое действие.  Для бинарной операции @ выражение x@=y означает
           x=x@y за исключением того,  что x вычисляется только один раз.  Это
           применимо к бинарным операциям
           
             + - * / % & | ^ << >>
           
           поэтому возможны следующие операции присваивания:
           
             += -= *= /= %= &= |= ^= <<= >>=
           
             Каждая является  отдельной лексемой,  поэтому 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 вводится в программе там, где она
           нужна, и сразу же инициализируется. Во многих языках описание может
           располагаться только в голове блока.  Это ограничение может  приво-
           дить  к  довольно  скверному искажению стиля программирования и/или
           излишним ошибкам.  Чаще всего неинициализированные локальные  пере-
           менные  являются просто признаком плохого стиля;  исключением явля-
           ются переменные,  подлежащие инициализации посредством ввода, и пе-
           ременные  векторного  или структурного типа,  которые нельзя удобно
           инициализировать одними присваиваниями*.  Заметьте,  что = является
           операцией присваивания, а == операцией сравнения.
           
           ────────────────────
           * В языке немного лучше этого с этими исключениями тоже надо бы
           справляться. (прим.  автора) Функция 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' && isspace(ch));
           
             Вызов cin.get(ch)  считывает  один  символ из стандартного потока
           ввода в ch. Проверка if(!cin.get(ch)) не проходит в случае, если из
           cin  нельзя  считать ни одного символа.  В этом случае возвращается
           END, чтобы завершить сеанс работы калькулятора. Используется опера-
           ция  !  (НЕ),  поскольку get() возвращает в случае успеха ненулевое
           значение.
           
             Функция (inline) isspace() из <ctype.h> обеспечивает  стандартную
           проверку на то,  является ли символ пропуском (#8.4.1);  isspace(c)
           возвращает ненулевое значение, если c является символом пропуска, и
           ноль в противном случае.  Проверка реализуется в виде поиска в таб-
           лице, поэтому использование isspace() намного быстрее, чем проверка
           на  отдельные  символы  пропуска;  это  же  относится  и к функциям
           isalpha(),  isdigit()   и   isalnum(),   которые   используются   в
           get_token().
             После того,  как пустое место пропущено, следующий символ исполь-
           зуется для определения того, какого вида какого вида лексема прихо-
           дит.  Давайте сначала рассмотрим некоторые случаи отдельно,  прежде
           чем приводить всю функцию. Ограничители лексем '\n' и ';' обрабаты-
           ваются так:
           
             switch (ch) { case ';': case '\n':
                 cin >> WS; // пропустить пропуск return curr_tok=PRINT;
           
             Пропуск пустого места делать необязательно, но он позволяет избе-
           жать повторных обращений к get_token().  WS - это стандартный  про-
           пусковый объект, описанный в <stream.h>; он используется только для
           сброса пропуска. Ошибка во вводе или конец ввода не будут обнаруже-
           ны до следующего обращения к get_token().  Обратите внимание на то,
           как можно использовать несколько меток case (случаев) для  одной  и
           той же последовательности операторов,  обрабатывающих эти случаи. В
           обоих случаях возвращается лексема PRINT и помещается в curr_tok.
           
             Числа обрабатываются так:
           
             case '0': case '1': case '2': case '3': case '4':
             case '5': case '6': case '7': case '8': case '9':
             case '.':
                 cin.putback(ch); cin >> number_value; return curr_tok=NUMBER;
           
             Располагать метки  случаев case горизонтально,  а не вертикально,
           не очень хорошая мысль,  поскольку читать это гораздо  труднее,  но
           отводить по одной строке на каждую цифру нудно.
           
             Поскольку операция  >>  определена  также и для чтения констант с
           плавающей точкой в double,  программирование  этого  не  составляет
           труда: сперва начальный символ (цифра или точка) помещается обратно
           в cin, а затем можно считывать константу в number_value.
           
             Имя, то есть лексема NAME,  определяется как  буква,  за  которой
           возможно следует несколько букв или цифр:
           
             if (isalpha(ch))  {  char*  p  =  name_string;  *p++ = ch;  while
                 (cin.get(ch) && isalnum(ch)) *p++ = ch; cin.putback(ch); *p =
                 0; return curr_tok=NAME;
             }
             Эта часть  строит  в  name_string строку,  заканчивающуюся нулем.
           Функции isalpha() и isalnum() заданы  в  <ctype.h>;  isalnum(c)  не
           ноль, если c буква или цифра, ноль в противном случае.
           
             Вот, наконец, функция ввода полностью:
           
             token_value get_token() {
                 char ch;
           
                 do {   //   пропускает   пропуски   за    исключением    '\n'
                     if(!cin.get(ch)) return curr_tok = END;
                 } while (ch!='\n' && isspace(ch));
           
                 switch (ch) { case ';': case '\n':
                     cin >> WS; // пропустить пропуск return curr_tok=PRINT;
                 case '*':
                 case '/':
                 case '+':
                 case '-':
                 case '(':
                 case ')':
                 case '=':
                     return curr_tok=ch;  case '0':  case '1':  case '2': case
                 '3':  case '4':  case '5': case '6': case '7': case '8': case
                 '9': case '.':
                     cin.putback(ch); cin     >>     number_value;      return
                     curr_tok=NUMBER;
                 default: // NAME, NAME= или ошибка if (isalpha(ch)) {
                         char* p = name_string;
                         *p++ = ch;
                         while (cin.get(ch)   &&   isalnum(ch))   *p++  =  ch;
                         cin.putback(ch); *p = 0; return curr_tok=NAME;
                     }
                     error("плохая лексема"); return curr_tok=PRINT;
                 }
             }
           
             Поскольку token_value (значение лексемы) операции было определено
           как  целое значение этой операции*,  обработка всех операций триви-
           альна.
           
           ────────────────────
           * знака этой операции. (прим. перев.)
                3.1.3 Таблица имен
           
             К таблице имен доступ осуществляется с помощью одной функции
           
             name* look(char* p, int ins =0);
           
             Ее второй параметр указывает,  нужно ли сначала поместить  строку
           символов в таблицу.  Инициализатор =0 задает параметр, который над-
           лежит использовать по умолчанию,  когда look() вызывается  с  одним
           параметром.  Это дает удобство записи, когда look("sqrt2") означает
           look("sqrt2",0),  то есть просмотр,  без помещения в таблицу. Чтобы
           получить такое же удобство записи для помещения в таблицу,  опреде-
           ляется вторая функция:
           
             inline name* insert(char* s) { return look(s,1);}
           
             Как уже отмечалось раньше, элементы этой таблицы имеют тип:
           
             srtuct name { char* string; char* next; double value;
             }
           
             Член next  используется только для сцепления вместе имен в табли-
           це.
           
             Сама таблица - это просто вектор указателей на объекты типа name:
           
             const TBLSZ = 23; name* table[TBLSZ];
           
             Поскольку все статические  объекты  инициализируются  нулем,  это
           тривиальное  описание  таблицы  table  гарантирует также надлежащую
           инициализацию.
             Для нахождения  элемента  в  таблице в look() принимается простой
           алгоритм хэширования (имена с одним и тем же хэш-кодом  зацепляются
           вместе):
           
             int ii = 0; // хэширование char* pp = p; while (*pp) ii = ii<<1 ^
             *pp++; if (ii < 0) ii = -ii; ii %= TBLSZ;
           
             То есть,  с помощью исключающего ИЛИ  каждый  символ  во  входной
           строке "добавляется" к ii ("сумме" предыдущих символов).  Бит в x^y
           устанавливается единичным тогда и только тогда, когда соответствую-
           щие биты в x и y различны. Перед применением в символе исключающего
           ИЛИ, ii сдвигается на один бит влево, чтобы не использовать в слове
           только один байт. Это можно было написать и так:
           
             ii <<= 1; ii ^= *pp++;
           
             Кстати, применение ^ лучше и быстрее,  чем +. Сдвиг важен для по-
           лучения приемлемого хэш-кода в обоих случаях. Операторы
           
             if (ii < 0) ii = -ii; ii %= TBLSZ;
           
           обеспечивают, что ii будет лежать в диапазоне 0...TBLSZ-1;  % - это
           операция взятия по модулю (еще называемая получением остатка).
             Вот функция полностью:
           
             extern int strlen(const char*);  extern int  strcmp(const  char*,
             const char*); extern int strcpy(const char*, const char*);
           
             name* look(char* p, int ins =0) {
                 int ii = 0;  // хэширование char* pp = p;  while (*pp)  ii  =
                 ii<<1 ^ *pp++; if (ii < 0) ii = -ii; ii %= TBLSZ;
           
                 for (name*   n=table[ii];   n;   n=n->next)   //   поиск   if
                     (strcmp(p,n->string) == 0) return n;
           
                 if (ins == 0) error("имя не найдено");
           
                 name* nn  =  new  name;   //   вставка   nn->string   =   new
                 char[strlen(p)+1];   strcpy(nn->string,p);   nn->value  =  1;
                 nn->next = table[ii]; table[ii] = nn; return nn;
             }
           
             После вычисления хэш-кода ii имя находится простым просмотром че-
           рез поля next. Проверка каждого name осуществляется с помощью стан-
           дартной  функции  strcmp().  Если  строка найдена,  возвращается ее
           name, иначе добавляется новое name.
           
             Добавление нового name включает в себя создание нового объекта  в
           свободной памяти с помощью операции new (см. #3.2.6), его инициали-
           зацию,  и добавление его к списку  имен.  Последнее  осуществляется
           просто путем помещения нового имени в голову списка,  поскольку это
           можно делать даже не проверяя,  имеется список, или нет. Символьную
           строку  для имени тоже нужно сохранить в свободной памяти.  Функция
           strlen() используется для определения того,  сколько памяти  нужно,
           new - для выделения этой памяти, и strcpy() - для копирования стро-
           ки в память.
                3.1.4 Обработка ошибок
           
             Поскольку программа  так  проста,  обработка ошибок не составляет
           большого труда. Функция обработки ошибок просто считает ошибки, пи-
           шет сообщение об ошибке и возвращает управление обратно:
           
             int no_of_errors;
           
             double error(char*  s)  {  cerr  <<  "error:  "  <<  s  <<  "\n";
                 no_of_errors++; return 1;
             }
           
             Возвращается значение потому, что ошибки обычно встречаются в се-
           редине вычисления выражения,  и поэтому надо либо полностью прекра-
           щать  вычисление,  либо возвращать значение,  которое по всей види-
           мости не должно вызвать последующих ошибок.  Для простого калькуля-
           тора больше подходит последнее. Если бы get_token() отслеживала но-
           мера строк,  то error() могла бы сообщать пользователю, где прибли-
           зительно обнаружена ошибка.  Это наверняка было бы полезно, если бы
           калькулятор использовался неитерактивно.
           
             Часто бывает так, что после появления ошибки программа должна за-
           вершиться, поскольку нет никакого разумного пути продолжить работу.
           Это можно сделать с помощью вызова exit(), которая очищает все вро-
           де  потоков вывода (#8.3.2),  а затем завершает программу используя
           свой параметр в качестве ее возвращаемого значения. Более радикаль-
           ный способ завершения программы - это вызов abort(),  которая обры-
           вает выполнение сразу же или сразу после сохранения где-то информа-
           ции для отладчика (дамп памяти);  о подробностях справьтесь,  пожа-
           луйста, в вашем руководстве.
           
                3.1.5 Драйвер
           
             Когда все части программы на месте,  нам нужен только драйвер для
           инициализации и всего того,  что связано с запуском. В этом простом
           примере main() может работать так:
           
             int main() {
                 // вставить предопределенные имена:
                 insert("pi")->value =                  3.1415926535897932385;
                 insert("e")->value = 2.7182818284590452354;
           
                 while (cin)  {  get_token();  if (curr_tok == END) break;  if
                     (curr_tok == PRINT) continue; cout << expr() << "\n";
                 }
                 return no_of_errors;
             }
           
             Принято обычно,  что main() возвращает ноль при нормальном завер-
           шении программы и не ноль в противном случае, поэтому это прекрасно
           может  сделать  возвращение числа ошибок.  В данном случае оказыва-
           ется,  что инициализация нужна только для введения предопределенных
           имен в таблицу имен.
             Основная работа цикла - читать выражения и писать ответ.  Это де-
           лает строка:
           
             cout << expr() << "\n";
           
             Проверка cin  на  каждом  проходе  цикла  обеспечивает завершение
           программы в случае,  если с потоком ввода что-то не так, а проверка
           на  END  обеспечивает корректный выход из цикла,  когда get_token()
           встречает конец файла. Оператор break осуществляет выход из ближай-
           шего содержащего его оператора switch или оператора цикла (то есть,
           оператора for, оператора while или оператора do). Проверка на PRINT
           (то есть,  на '\n' или ';') освобождает expr() от обязанности обра-
           батывать пустые выражения.  Оператор continue равносилен переходу к
           самому концу цикла, поэтому в данном случае
           
             while (cin) {
                 // ...
                 if (curr_tok == PRINT) continue; cout << expr() << "\n";
             }
           
           эквивалентно
           
             while (cin) {
                 // ...
                 if (curr_tok == PRINT) goto end_of_loop;  cout <<  expr()  <<
                 "\n"; end_of_loop
             }
           
             Более подробно циклы описываются в #с.9.
           
                3.1.6 Параметры командной строки
           
             После того,  как программа была написана и оттестирована, я заме-
           тил,  что часто набирать выражения на клавиатуре в стандартный ввод
           надоедает,  поскольку обычно использование программы состоит в  вы-
           числении одного выражения.  Если бы можно было представлять это вы-
           ражение как параметр командной строки,  не приходилось бы так много
           нажимать на клавиши.
           
             Как уже говорилось,  программа запускается вызовом main().  Когда
           это происходит, main() получает два параметра указывающий число па-
           раметров,  обычно называемый argc и вектор параметров, обычно назы-
           ваемый argv.  Параметры - это символьные строки, поэтому argv имеет
           тип  char*[argc].  Имя  программы  (так,  как оно стоит в командной
           строке) передается в качестве argv[0], поэтому argc всегда не мень-
           ше единицы. Например, в случае команды
           
             dc 150/1.1934
           
           параметры имеют значения:
           
             argc 2  argv[0]  "dc" argv[1] "150/1.1934" Научиться пользоваться
             параметрами командной строки
           несложно. Сложность  состоит в том,  как использовать их без переп-
           рограммирования.  В данном случае это  оказывается  совсем  просто,
           поскольку  поток  ввода можно связать с символьной строкой,  а не с
           файлом (#8.5).  Например,  можно заставить cin  читать  символы  из
           стандартного ввода:
           
             int main(int argc, char* argv[]) {
                 switch(argc) { case 1: // читать из стандартного ввода
                     break; case 2: // читать параметр строку
                     cin = *new istream(strlen(argv[1]),argv[1]); break;
                 default:
                     error("слишком много параметров"); return 1;
                 }
                 // как раньше
             }
           
             Программа осталась  без  изменений,  за  исключением добавления в
           main() параметров  и  использования  этих  параметров  в  операторе
           switch.  Можно  было бы легко модифицировать main() так,  чтобы она
           получала несколько параметров командной строки,  но это оказывается
           ненужным, особенно потому, что несколько выражений можно передавать
           как один параметр:
           
             dc "rate=1.1934;150/rate;19.75/rate;217/rate"
           
             Здесь кавычки необходимы,  поскольку ;  является разделителем ко-
           манд в системе UNIX.
           
                3.2 Краткая сводка операций
           
             Операции С++ подробно и систематически описываются в #с.7; прочи-
           тайте,  пожалуйста, этот раздел. Здесь же приводится операция крат-
           кая сводка и некоторые примеры. После каждой операции приведено од-
           но или более ее общеупотребительных названий и пример ее  использо-
           вания. В этих примерах имя_класса - это имя класса, член - имя чле-
           на,  объект - выражение,  дающее в результате объект класса, указа-
           тель - выражение, дающее в результате указатель, выр - выражение, а
           lvalue - выражение,  денотирующее неконстантный объект.  Тип  может
           быть  совершенно  произвольным  именем типа (со * () и т.п.) только
           когда он стоит в скобках,  во всех остальных случаях существуют ог-
           раничения.
           
             Унарные операции  и операции присваивания правоассоциативны,  все
           остальные левоассоциативны. Это значит, что a=b=c означает a=(b=c),
           a+b+c означает (a+b)+c, и *p++ означает *(p++), а не (*p)++.
                        Сводка Операций (часть 1)
           ┌───────────────────────────────────────────────────────────────┐
           │  :: разрешение области видимости имя_класса :: член           │
           │  :: глобальное :: имя                                         │
           ├───────────────────────────────────────────────────────────────┤
           │  -> выбор члена указатель->член                               │
           │  [] индексация указатель [ выр ]                              │
           │  () вызов функции выр (список_выр)                            │
           │  () построение значения тип (список_выр)                      │
           │  sizeof размер объекта sizeof выр                             │
           │  sizeof размер типа sizeof ( тип )                            │
           ├───────────────────────────────────────────────────────────────┤
           │  ++ приращение после lvalue++                                 │
           │  ++ приращение до ++lvalue                                    │
           │  -- уменьшение после lvalue--                                 │
           │  -- уменьшение до --lvalue                                    │
           │  ~ дополнение ~ выр                                           │
           │  ! не ! выр                                                   │
           │  - унарный минус - выр                                        │
           │  + унарный плюс + выр                                         │
           │  & адрес объекта & lvalue                                     │
           │  * разыменование * выр                                        │
           │  new создание (размещение) new тип                            │
           │  delete уничтожение (освобождение) delete указатель           │
           │  delete[] уничтожение вектора delete[ выр ] указатель         │
           │  () приведение (преобразование типа) ( тип ) выр              │
           ├───────────────────────────────────────────────────────────────┤
           │  * умножение выр * выр                                        │
           │  / деление выр / выр                                          │
           │  % взятие по модулю (остаток) выр % выр                       │
           ├───────────────────────────────────────────────────────────────┤
           │  + сложение (плюс) выр + выр                                  │
           │  - вычитание (минус) выр - выр                                │
           └───────────────────────────────────────────────────────────────┘
           
             В каждой очерченной части находятся операции с одинаковым приори-
           тетом.  Операция имеет приоритет больше,  чем операции  из  частей,
           расположенных  ниже.  Например:  a+b*c означает a+(b*c),  так как *
           имеет приоритет выше,  чем +, а a+b-c означает (a+b)-c, поскольку +
           и - имеют одинаковый приоритет (и поскольку + левоассоциативен).
                       Сводка Операций (часть 2)
           ┌───────────────────────────────────────────────────────────────┐
           │  << сдвиг влево lvalue << выр                                 │
           │  >> сдвиг вправо lvalue >> выр                                │
           ├───────────────────────────────────────────────────────────────┤
           │  < меньше выр < выр                                           │
           │  <= меньше или равно выр <= выр                               │
           │  > больше выр > выр                                           │
           │  >= больше или равно выр >= выр                               │
           ├───────────────────────────────────────────────────────────────┤
           │  == равно выр == выр                                          │
           │  != не равно выр != выр                                       │
           ├───────────────────────────────────────────────────────────────┤
           │  & побитовое И выр & выр                                      │
           ├───────────────────────────────────────────────────────────────┤
           │  ^ побитовое исключающее ИЛИ выр ^ выр                        │
           ├───────────────────────────────────────────────────────────────┤
           │  | побитовое включающее ИЛИ выр | выр                         │
           ├───────────────────────────────────────────────────────────────┤
           │  && логическое И выр && выр                                   │
           ├───────────────────────────────────────────────────────────────┤
           │  || логическое включающее ИЛИ выр || выр                      │
           ├───────────────────────────────────────────────────────────────┤
           │  ? : арифметический if выр ? выр : выр                        │
           ├───────────────────────────────────────────────────────────────┤
           │  = простое присваивание lvalue = выр                          │
           │  *= умножить и присвоить lvalue = выр                         │
           │  /= разделить и присвоить lvalue /= выр                       │
           │  %= взять по модулю и присвоить lvalue %= выр                 │
           │  += сложить и присвоить lvalue += выр                         │
           │  -= вычесть и присвоить lvalue -= выр                         │
           │  <<= сдвинуть влево и присвоить lvalue <<= выр                │
           │  >>= сдвинуть вправо и присвоить lvalue >>= выр               │
           │  &= И и присвоить lvalue &= выр                               │
           │  |= включающее ИЛИ и присвоить lvalue |= выр                  │
           │  ^= исключающее ИЛИ и присвоить lvalue ^= выр                 │
           ├───────────────────────────────────────────────────────────────┤
           │  , запятая (следование) выр , выр                             │
           └───────────────────────────────────────────────────────────────┘
                3.2.1 Круглые скобки
           
             Скобками синтаксис  С++  злоупотребляет;  количество  способов их
           использования приводит в замешательство: они применяются для заклю-
           чения  в них параметров в вызовах функций,  в них заключается тип в
           преобразовании типа (приведении к типу), в именах типов для обозна-
           чения  функций,  а  также для разрешения конфликтов приоритетов.  К
           счастью,  последнее требуется не слишком часто,  потому что  уровни
           приоритета и правила ассоциативности определены таким образом, что-
           бы выражения "работали ожидаемым образом" (то есть, отражали наибо-
           лее привычный способ употребления). Например, значение
           
             if (i<=0 || max<i) // ...
           
           очевидно. Тем не менее,  всегда,  когда программист сомневается от-
           носительно этих правил,  следует употреблять  скобки,  и  некоторые
           программисты предпочитают немного более длинное и менее элегантное
           
             if ( (i<=0) || (max<i) ) // ...
           
             При усложнении  подвыражений употребление скобок становится более
           обычным явлением,  но сложные подвыражения являются источником оши-
           бок,  поэтому если вы чувствуете потребность в скобках,  попробуйте
           оборвать выражение и использовать дополнительную переменную. Есть и
           такие случаи,  когда приоритеты операций не приводят к "очевидному"
           результату. Например в
           
             if (i&mask == 0) // ...
           
           не происходит применения маски mask к i и последующей проверки  ре-
           зультата на ноль. Поскольку == имеет приоритет выше, чем &, выраже-
           ние интерпретируется как i&(mask==0). В этом случае скобки оказыва-
           ются важны:
           
             if ((i&mask) == 0) // ...
           
             Но, с  другой  стороны,  то,  что следующее выражение не работает
           так, как может ожидать наивный пользователь, ничего не значит:
           
             if (0 <= a <= 99) // ...
           
           Оно допустимо,  но интерпретируется оно как (0<=a)<=99, где резуль-
           тат  первого  подвыражения  или 0 или 1,  но не a (если только a не
           равно 1). Чтобы проверить, лежит ли a в диапазоне 0...99, можно на-
           писать
           
             if (0<=a && a<=99) // ...
                3.2.2 Порядок вычисления
           
             Порядок вычисления подвыражений в выражении неопределен. Например
           
             int i = 1; v[i] = i++;
           
           может вычисляться или как v[1]=1,  или как v[2]=1.  При  отсутствии
           ограничений  на  порядок  вычисления выражения может генерироваться
           более хороший код. Было бы замечательно, если бы компилятор предуп-
           реждал  о  подобных  неоднозначностях,  но большинство компиляторов
           этого не делают.
           
             Относительно операций && и || гарантируется, что их левый операнд
           вычисляется раньше, чем правый. Например, b=(a=2,a=1) присвоит b 3.
           В #3.3.1 приводятся примеры использования && и  ||.  Заметьте,  что
           операция  следования  ,  (запятая) логически отличается от запятой,
           которая используется для разделения параметров  в  вызове  функции.
           Рассмотрим
           
             f1(v[i],i++); // два параметра f2( (v[i],i++) ) // один параметр
           
           В вызове f1 два параметра, v[i] и i++, и порядок вычисления выраже-
           ний-параметров неопределен.  Зависимость выражения-параметра от по-
           рядка вычисления - это очень плохой стиль,  а также непереносимо. В
           вызове f2 один параметр,  выражение с запятой, которое эквивалентно
           i++.
           
             С помощью  скобок  нельзя  задать  порядок вычисления.  Например,
           a*(b/c) может вычисляться и как (a*b)/c, поскольку * и / имеют оди-
           наковый приоритет.  В тех случаях,  когда важен порядок вычисления,
           можно вводить дополнительную переменную, например, (t=b/c,a*t).
           
                3.2.3 Увеличение и уменьшение*
           
           ────────────────────
             * Следовало бы переводить как "инкремент" и "декремент", однако
           мы следовали терминологии,  принятой в переводной литературе по  C,
           поскольку эти операции унаследованы от C. (прим. перев.)
             Операция ++ используется для явного выражения  приращения  вместо
           его  неявного  выражения с помощью комбинации сложения и присваива-
           ния. По определению ++lvalue означает lvalue+=1, что в свою очередь
           означает lvalue=lvalue+1 при условии,  что lvalue не вызывает ника-
           ких побочных эффектов.  Выражение, обозначающее (денотирующее) объ-
           ект,  который должен быть увеличен,  вычисляется один раз (только).
           Аналогично,  уменьшение выражается операцией --.  Операции ++ и  --
           могут  применяться и как префиксные,  и как постфиксные.  Значением
           ++x является новое (то  есть  увеличенное)  значение  x.  Например,
           y=++x эквивалентно y=(x+=1).  Значение x++,  напротив,  есть старое
           значение x.  Например,  y=x++ эквивалентно y=(t=x,x+=1,t),  где t -
           переменная того же типа, что и x.
           
             Операции приращения  особенно полезны для увеличения и уменьшения
           переменных в циклах.  Например,  оканчивающуюся нулем строку  можно
           копировать так:
           
             inline void cpy(char* p, const char* q) {
                 while (*p++ = *q++) ;
             }
           
             Напомню, что  увеличение  и уменьшение арифметических указателей,
           так же как сложение и вычитание указателей, осуществляется в терми-
           нах элементов вектора,  на которые указывает указатель p++ приводит
           к тому,  что p указывает на следующий элемент. Для указателя p типа
           T* по определению выполняется следующее:
           
             long(p+1) == long(p)+sizeof(T);
           
                3.2.4 Побитовые логические операции
           
             Побитовые логические операции
           
             & | ^ ~ >> <<
           
           применяются к целым, то есть к объектам типа char, short, int, long
           и их unsigned аналогам, результаты тоже целые.
           
             Одно из стандартных применений побитовых  логических  операций  -
           реализация маленького множества (вектор битов).  В этом случае каж-
           дый бит беззнакового целого представляет  один  член  множества,  а
           число членов ограничено числом битов. Бинарная операция & интерпре-
           тируется как пересечение,  | как объединение, а ^ как разность. Для
           наименования  членов такого множества можно использовать перечисле-
           ние. Вот маленький пример, заимствованный из реализации (не пользо-
           вательского интерфейса) <stream.h>:
           
             enum state_value { _good=0, _eof=1, _fail=2, _bad=4 };
                              // хорошо, конец файла, ошибка, плохо
           
             Определение _good не является необходимым.  Я просто хотел, чтобы
           состояние, когда все в порядке, имело подходящее имя. Состояние по-
           тока можно установить заново следующим образом:
           
             cout.state = _good;
           
             Например, так можно проверить, не был ли испорчен поток или допу-
           щена операционная ошибка:
           
             if (cout.state&(_bad|_fail)) // не good
           
           Еще одни скобки необходимы, поскольку & имеет более высокий приори-
           тет, чем |.
             Функция, достигающая конца ввода, может сообщать об этом так:
           
             cin.state |= _eof;
           
           Операция |= используется потому,  что поток уже может быть испорчен
           (то есть, state==_bad), поэтому
           
             cin.state = _eof;
           
           очистило бы этот признак. Различие двух потоков можно находить так:
           
             state_value diff = cin.state^cout.state;
           
             В случае типа stream_state (состояние потока) такая  разность  не
           очень нужна,  но для других похожих типов она оказывается самой по-
           лезной.  Например,  при сравнении вектора бит, представляющего мно-
           жество прерываний, которые обрабатываются, с другим, представляющим
           прерывания, ждущие обработки.
           
             Следует заметить, что использование полей (#2.5.1) в действитель-
           ности является сокращенной записью сдвига и маскирования для извле-
           чения полей бит из слова.  Это,  конечно, можно сделать и с помощью
           побитовых логических операций,  Например, извлечь средние 16 бит из
           32-битового int можно следующим образом:
           
             unsigned short middle(int a) { return (a>>8)&0xffff; }
           
             Не путайте побитовые логические операции с логическими  операция-
             ми:
           
               && || !
           
             Последние возвращают 0 или 1,  и они главным образом используются
           для записи проверки в операторах if, while или for (#3.3.1). Напри-
           мер,  !0 (не ноль) есть значение 1,  тогда как ~0 (дополнение нуля)
           есть набор битов все-единицы, который обычно является значением -1.
                3.2.5 Преобразование типа
           
             Бывает необходимо  явно преобразовать значение одного типа в зна-
           чение другого.  Явное преобразование типа дает значение одного типа
           для данного значения другого типа. Например:
           
             float r = float(1);
           
           перед присваиванием  преобразует целое значение 1 к значению с пла-
           вающей  точкой  1.0.  Результат  преобразования  типа  не  является
           lvalue, поэтому ему нельзя присваивать (если только тип не является
           ссылочным типом).
           
             Есть два способа записи явного преобразования типа:  традиционная
           в  C  запись  приведения  к  типу (double)a и функциональная запись
           double(a).  Функциональная запись не может применяться  для  типов,
           которые не имеют простого имени. Например, чтобы преобразовать зна-
           чение к указательному типу надо или использовать запись  преобразо-
           вания типа
           
             char* p = (char*)0777;
           
           или определить новое имя типа:
           
             typedef char*  Pchar;  char*  p  = Pchar(0777);  По моему мнению,
             функциональная запись в нетривиальных случаях
           предпочтительна. Рассмотрим два эквивалентных примера
           
             Pname n2 = Pbase(n1->tp)->b_name;  // функциональная запись Pname
             n3 = ((Pbase)n2->tp)->b_name; // запись приведения к типу
           
             Поскольку операция -> имеет больший  приоритет,  чем  приведение,
           последнее выражение интерпретируется как
           
             ((Pbase)(n2->tp))->b_name
           
             С помощью  явного  преобразования типа к указательным типам можно
           симитировать,  что объект имеет совершенно произвольный тип. Напри-
           мер:
           
             any_type* p = (any_type*)&some_object;
           
           позволит работать  посредством  p  с некоторым объектом some_object
           как с любым типом any_type.
           
             Когда преобразование типа не необходимо,  его  следует  избегать.
           Программы, в которых используется много явных преобразований типов,
           труднее понимать,  чем те,  в которых это не делается. Однако такие
           программы легче понимать, чем программы, просто не использующие ти-
           пы для представления понятий более высокого уровня (например, прог-
           рамму,  которая  оперирует  регистром устройства с помощью сдвига и
           маскирования,  вместо того,  чтобы определить подходящую  struct  и
           оперировать ею, см. #2.5.2). Кроме того, правильность явного преоб-
           разования типа часто критическим образом зависит от понимания прог-
           раммистом  того,  каким образом объекты различных типов обрабатыва-
           ются в языке, и очень часто от подробностей реализации. Например:
           
             int i = 1; char* pc = "asdf"; int* pi = &i;
           
             i = (int)pc;  pc = (char*)i;  // остерегайтесь! значение pc может
             измениться
                               // на некоторых машинах
                               // sizeof(int)<sizeof(char*)
             pi = (int*)pc;  pc = (char*)pi; // остерегайтесь! значение pc мо-
             жет измениться
                               // на некоторых машинах char*
                               // представляется иначе, чем int*
           
             На многих машинах ничего плохого не произойдет,  но на других ре-
           зультаты будут катастрофическими.  Этот код в лучшем случае непере-
           носим.  Обычно можно без риска предполагать,  что указатели на раз-
           личные структуры имеют одинаковое представление.  Кроме того, любой
           указатель можно (без явного преобразования типа) присвоить void*, а
           void* можно явно преобразовать к указателю любого типа.
           
             В С++ явное преобразование типа оказывается  ненужным  во  многих
           случаях, когда C (и другие языки) требуют его. Во многих программах
           явного преобразования типа можно совсем избежать,  а во многих дру-
           гих  его  применение  можно локализовать в небольшом числе подпрог-
           рамм.
                3.2.6 Свободная память
           
             Именованный объект является либо статическим, либо автоматическим
           (см. #2.1.3). Статический объект размещается во время запуска прог-
           раммы и существует в течение всего выполнения программы.  Автомати-
           ческий объект размещается каждый раз при входе в его блок и сущест-
           вует только до тех пор,  пока из этого блока не вышли. Однако часто
           бывает полезно создать новый объект,  существующий до тех пор, пока
           он  не станет больше не нужен.  В частности,  часто полезно создать
           объект,  который можно использовать после возврата из функции,  где
           он  создается.  Такие объекты создает операция new,  а впоследствии
           уничтожать их можно операцией delete. Про объекты, выделенные с по-
           мощью  операции new,  говорят,  что они в свободной памяти.  Такими
           объектами обычно являются вершины деревьев или  элементы  связанных
           списков, являющиеся частью большей структуры данных, размер которой
           не может быть известен на стадии компиляции.  Рассмотрим, как можно
           было  бы написать компилятор в духе написанного настольного кальку-
           лятора.  Функции синтаксического анализа могут строить  древовидное
           представление выражений, которое будет использоваться при генерации
           кода. Например:
           
             struct enode { token_value oper; enode* left; enode* right;
             };
           
             enode* expr() { enode* left = term();
           
             for(;;) switch(curr_tok) { case PLUS: case MINUS:
                     get_token(); enode* n = new enode;  n->oper  =  curr_tok;
                     n->left = left; n->right = term(); left = n; break;
                 default:
                     return left;
                 }
             }
           
             Получающееся дерево  генератор  кода  может использовать например
           так:
           
             void generate(enode* n) {
                 switch (n->oper) { case PLUS:
                     // делает нечто соответствующее
                     delete n;
                 }
             }
             Объект, созданный с помощью new, существует, пока он не будет яв-
           но уничтожен delete,  после чего пространство,  которое он занимал,
           опять может использоваться new. Никакого "сборщика мусора", который
           ищет объекты,  на которые нет ссылок, и предоставляет их в распоря-
           жение new, нет. Операция delete может применяться только к указате-
           лю,  который  был возвращен операцией new,  или к нулю.  Применение
           delete к нулю не вызывает никаких действий.
           
             С помощью new можно также создавать вектора объектов. Например:
           
             char* save_string(char* p) {
                 char* s = new char[strlen(p)+1]; strcpy(s,p); return s;
             }
           
             Следует заметить,  что чтобы освободить пространство,  выделенное
           new,  delete должна иметь возможность определить размер выделенного
           объекта. Например:
           
             int main(int argc, char* argv[]) {
                 if (argc < 2) exit(1); char* p = save_string(argv[1]); delete
                 p;
             }
           
             Это приводит к тому,  что объект, выделенный стандартной реализа-
           цией new,  будет занимать  больше  места,  чем  статический  объект
           (обычно, больше на одно слово).
           
             Можно также  явно указывать размер вектора в операции уничтожения
           delete. Например:
           
             int main(int argc, char* argv[]) {
                 if (argc < 2) exit(1);  int size = strlen(argv[1])+1; char* p
                 = save_string(argv[1]); delete[size] p;
             }
           
             Заданный пользователем размер вектора игнорируется за исключением
           некоторых типов, определяемых пользователем (#5.5.5).
           
             Операции свободной памяти реализуются функциями (#с.7.2.3):
           
             void operator new(long); void operator delete(void*);
           
             Стандартная реализация new не инициализирует возвращаемый объект.
             Что происходит,  когда  new  не  находит  памяти  для  выделения?
           Поскольку даже виртуальная память конечна,  это иногда должно  про-
           исходить. Запрос вроде
           
             char* p = new char[100000000];
           
           как правило,  приводит к каким-то неприятностям. Когда у new ничего
           не  получается,  она  вызывает  функцию,   указываемую   указателем
           _new_handler (указатели на функции обсуждаются в #4.6.9). Вы можете
           задать указатель явно или использовать  функцию  set_new_handler().
           Например:
           
             #include <stream.h>
           
             void out_of_store() {
                 cerr << "операция new  не  прошла:  за  пределами  памяти\n";
                 exit(1);
             }
           
             typedef void (*PF)(); // тип указатель на функцию
           
             extern PF set_new_handler(PF);
           
             main() {
                 set_new_handler(out_of_store); char* p = new char[100000000];
                 cout << "сделано, p = " << long(p) << "\n";
             }
           
           как правило,  не будет писать "сделано", а будет вместо этого выда-
           вать
           
               операция new не прошла: за пределами памяти
           
             Функция _new_handler может делать и кое-что поумней,  чем  просто
           завершать выполнение программы.  Если вы знаете, как работают new и
           delete,  например,  потому, что вы задали свои собственные operator
           new()  и  operator  delete(),  программа обработки может попытаться
           найти некоторое количество памяти,  которое возвратит new.  Другими
           словами,  пользователь может сделать сборщик мусора,  сделав, таким
           образом,  использование delete  необязательным.  Но  это,  конечно,
           все-таки задача не для начинающего.
             По историческим причинам new просто возвращает указатель 0,  если
           она не может найти достаточное количество памяти и не был задан ни-
           какой _new_handler. Например
           
             include <stream.h>
           
             main() {
                 char* p  =  new char[100000000];  cout << "сделано,  p = " <<
                 long(p) << "\n";
             }
           
           выдаст
           
             сделано, p = 0
           
             Вам сделали предупреждение! Заметьте, что тот, кто задает
           _new_handler, берет на себя заботу по проверке истощения памяти
           при каждом  использовании  new  в программе (за исключением случая,
           когда пользователь задал отдельные подпрограммы для размещения объ-
           ектов заданных типов, определяемых пользователем, см. #5.5.6).
                3.3 Сводка операторов
           
             Операторы С++ систематически и полностью изложены в #с.9,  прочи-
           тайте, пожалуйста, этот раздел. А здесь приводится краткая сводка и
           некоторые примеры.
           
                                      Синтаксис оператора
              ───────────────────────────────────────────────────────────────
              оператор:
                  описание {список_операторов opt} выражение opt
           
                  if оператор if ( выражение ) оператор
                  if ( выражение ) оператор else оператор switch оператор
              switch ( выражение ) оператор
           
                  while (  выражение  ) оператор do оператор while (выражение)
                  for ( оператор выражение opt ; выражение opt ) оператор
           
                  case константное_выражение :  оператор  default  :  оператор
                  break ; continue ;
           
                  return выражение opt ;
           
                  goto идентификатор ; идентификатор : оператор
           
                  список_операторов:
                      оператор оператор список_операторов
           
             Заметьте, что описание является оператором,  и что нет операторов
           присваивания и вызова процедуры. Присваивание и вызов функции обра-
           батываются как выражения.
           
                3.3.1 Проверки
           
             Проверка значения может осуществляться  или  оператором  if,  или
           оператором switch:
           
             if ( выражение ) оператор if ( выражение ) оператор else оператор
             switch ( выражение ) оператор
           
             В С++ нет отдельного булевского типа. Операции сравнения
           
             == != < <= > >=
           
           возвращают целое 1,  если сравнение истинно, иначе возвращают 0. Не
           так уж непривычно видеть, что ИСТИНА определена как 1, а ЛОЖЬ опре-
           делена как 0.
             В операторе  if  первый (или единственный) оператор выполняется в
           том случае, если выражение ненулевое, иначе выполняется второй опе-
           ратор (если он задан). Отсюда следует, что в качестве условия может
           использоваться любое целое выражение. В частности, если a целое, то
           
             if (a) // ...
           
           эквивалентно
           
             if (a != 0) // ...
           
             Логические операции && || ! наиболее часто используются в услови-
           ях.  Операции && и || не будут вычислять второй аргумент,  если это
           ненужно. Например:
           
             if (p && 1<p->count) // ...
           
           вначале проверяет,  является ли p не нулем,  и только если это так,
           то проверяет 1<p->count.
           
             Некоторые простые  операторы  if  могут быть с удобством заменены
           выражениями арифметического if. Например:
           
             if (a <= d) max = b;
             else max = a;
           
           лучше выражается так:
           
             max = (a<=b) ? b : a;
           
             Скобки вокруг условия необязательны,  но я считаю,  что когда они
           используются, программу легче читать.
           
             Некоторые простые операторы switch можно  по-другому  записать  в
           виде набора операторов if. Например:
           
             switch (val) { case 1:
                 f(); break;
             case 2; g(); break;
             default:
                 h(); break;
             }
           
           иначе можно было бы записать так:
           
             if (val == 1) f();
             else if (val == 2) g();
             else h();
           
             Смысл тот же,  однако первый вариант  (switch)  предпочтительнее,
           поскольку  в  этом  случае  явно  выражается сущность действия (со-
           поставление значения с рядом  констант).  Поэтому  в  нетривиальных
           случаях оператор switch читается легче.
             Заботьтесь о том,  что switch  должен  как-то  завершаться,  если
           только вы не хотите, чтобы выполнялся следующий case. Например:
           
             switch (val) { // осторожно case 1:
                 cout << "case 1\n"; case 2;
                 cout << "case 2\n"; default:
                 cout << "default: case не найден\n";
             }
           
           при val==1 напечатает
           
             case 1 case 2 default: case не найден
           
           к великому изумлению непосвященного. Самый обычный способ завершить
           случай - это break, иногда можно даже использовать goto. Например:
           
             switch (val) { // осторожно case 0:
                 cout << "case 0\n"; case1: case 1:
                 cout << "case 1\n"; return;
             case 2; cout << "case 2\n"; goto case1;
             default:
                 cout << "default: case не найден\n"; return;
             }
           
             При обращении к нему с val==2 выдаст
           
             case 2 case 1
           
             Заметьте, что метка case не подходит как метка для употребления в
           операторе goto:
           
             goto case 1;       // синтаксическая ошибка
                3.3.2 Goto
           
             С++ снабжен имеющим дурную репутацию оператором goto.
           
             goto идентификатор; идентификатор : оператор
           
             В общем,  в программировании высокого уровня он имеет очень  мало
           применений, но он может быть очень полезен, когда С++ программа ге-
           нерируется программой, а не пишется непосредственно человеком. Нап-
           ример,  операторы goto можно использовать в синтаксическом анализа-
           торе, порождаемом генератором синтаксических анализаторов. Оператор
           goto может быть также важен в тех редких случаях,  когда важна наи-
           лучшая эффективность,  например,  во внутреннем цикле  какой-нибудь
           программы, работающей в реальном времени.
           
             Одно из  немногих разумных применений состоит в выходе из вложен-
           ного цикла или переключателя (break лишь прекращает выполнение  са-
           мого внутреннего охватывающего его цикла или переключателя). Напри-
           мер:
           
             for (int i = 0; i<n; i++) for (int j = 0; j<m; j++)
                     if (nm[i][j] == a) goto found // найдено
                                                   // не найдено
             // ...
           
             found:                                // найдено
             // nm[i][j] == a
           
             Имеется также оператор continue,  который по сути делает  переход
           на конец оператора цикла, как объясняется в #3.1.5.
           
                3.4 Комментарии и Выравнивание
           
             Продуманное использование  комментариев и согласованное использо-
           вание отступов может сделать чтение и понимание  программы  намного
           более приятным. Существует несколько различных стилей согласованно-
           го использования отступов. Автор не видит никаких серьезных основа-
           ний предпочесть один другому (хотя как и у большинства, у меня есть
           свои предпочтения). Сказанное относится также и к стилю комментари-
           ев.
           
             Неправильное использование  комментариев  может серьезно повлиять
           на удобочитаемость программы,  Компилятор  не  понимает  содержание
           комментария,  поэтому он никаким способом не может убедиться в том,
           что комментарий
           
             [1] осмыслен,
           
             [2] описывает программу и
           
             [3] не устарел.
           
             Непонятные, двусмысленные и просто неправильные  комментарии  со-
           держатся в большинстве программ.  Плохой комментарий может быть ху-
           же, чем никакой.
             Если что-то можно сформулировать средствами самого языка, следует
           это сделать,  а не просто отметить в комментарии.  Данное замечание
           относится к комментариям вроде:
           
             // переменная "v" должна быть инициализирована.
           
             // переменная "v" должна использоваться только функцией "f()".
           
             // вызвать функцию init() перед вызовом
             // любой другой функции в этом файле.
           
             // вызовите функцию очистки "cleanup()" в конце вашей
             // программы.
           
             // не используйте функцию "wierd()".
           
             // функция "f()" получает два параметра.
           
             При правильном использовании С++ подобные комментарии как правило
           становятся ненужными. Чтобы предыдущие комментарии стали излишними,
           можно,  например,  использовать  правила  компоновки (#4.2) и види-
           мость, инициализацию и правила очистки для классов (см. #5.5.2).
           
             Если что-то было ясно сформулировано на языке,  второй раз упоми-
           нать это в комментарии не следует. Например:
           
             a = b+c; // a становится b+c count++; // увеличить счетчик
           
             Такие комментарии хуже чем просто излишни,  они увеличивают объем
           текста,  который надо прочитать,  они часто затуманивают  структуру
           программы, и они могут быть неправильными.
             Автор предпочитает:
           
             [1] Комментарий для каждого исходного файла, сообщающий, для чего
                в  целом  предназначены находящиеся в нем комментарии,  дающий
                ссылки на справочники и  руководства,  общие  рекомендации  по
                использованию и т.д.,
           
             [2] Комментарий для каждой нетривиальной функции, в котором сфор-
                мулировано ее назначение,  используемый алгоритм (если он нео-
                чевиден) и, быть может, что-то о принимаемых в ней предположе-
                ниях относительно среды выполнения,
           
             [3] Небольшое число комментариев в тех местах, где программа нео-
                чевидна и/или непереносима и
           
             [4] Очень мало что еще.
           
             Например:
           
             // tbl.c: Реализация таблицы имен
             /*
                 Гауссовское исключение  с  частичным  См.  Ralston:  "A first
                 course ..." стр. 411.
             */
           
             // swap() предполагает размещение стека AT&T sB20.
           
             /**************************************
           
                 Copyright (c) 1984 AT&T, Inc. All rights reserved
           
             ****************************************/
           
             Удачно подобранные и хорошо написанные комментарии - существенная
           часть программы. Написание хороших комментариев может быть столь же
           сложным, сколь и написание самой программы.
           
             Заметьте также,  что  если  в  функции используются исключительно
           комментарии //,  то любую часть этой функции можно закомментировать
           с помощью комментариев /* */, и наоборот.
                3.5 Упражнения
           
             1. (*1) Перепишите следующий оператор for в  виде  эквивалентного
                оператора while:
           
                  for (i=0;  i<max_length;  i++)  if  (input_line[i]  ==  '?')
                      quest_count++;
           
             2. (*1) Полностью расставьте скобки в следующих выражениях:
           
                  a = b + c * d << 2 & 8 a & 077 != 3 a == b || a == c && c  <
                  5  c  = x != 0 0 <= i < 7 f(1,2)+3 a = -1 + + b -- - 5 a = b
                  == c ++ a = b = c = 0 a[4][2] *= * b ? c : * d * 2 a-b,c=d
           
             3. (*2) Найдите пять различных конструкций С++,  значение которых
                неопределено.
           
             4. (*2) Найдите десять различных примеров непереносимой С++ прог-
                раммы.
           
             5. (*1) Что происходит в вашей системе,  если вы делите на  ноль?
                Что происходит при переполнении и потере значимости?
           
             6. (*1) Полностью расставьте скобки в следующих выражениях:
           
                  *p++
                  *--p
                  ++a--
                  (int*)p->m
                  *p.m
                  *a[i]
           
             7. (*2) Напишите  функции:  strlen(),  которая  возвращает  длину
                строки,  strcpy(),  которая  копирует одну строку в другую,  и
                strcmp(),  которая сравнивает две строки.  Разберитесь,  какие
                должны  быть  типы параметров и типы возвращаемых значений,  а
                потом сравните их со стандартными версиями,  которые описаны в
                <string.h> и в вашем руководстве.
           
             8. (*1) Посмотрите, как ваш компилятор реагирует на ошибки:
           
                  a := b+1; if (a = 3) // ... if (a&077 == 0) // ...
           
                Придумайте ошибки попроще, и посмотрите, как компилятор на них
                реагирует.
             9. (*2) Напишите функцию cat(), получающую два строковых парамет-
                ра и возвращающую строку, которая является конкатенацией пара-
                метров.  Используйте  new,  чтобы найти память для результата.
                Напишите функцию rev(), которая получает строку и переставляет
                в ней символы в обратном порядке. То есть, после вызова rev(p)
                последний символ p становится первым.
           
             10. (*2) Что делает следующая программа?
           
                  void send(register* to, register* from, register count)
                  // Полезные комментарии несомненно уничтожены.
                  { register n=(count+7)/8; switch (count%8) {
                          case 0:  do { *to++ = *from++;  case 7: do { *to++ =
                          *from++;  case 6: do { *to++ = *from++; case 5: do {
                          *to++ = *from++;  case 4: do { *to++ = *from++; case
                          3:  do { *to++ = *from++;  case  2:  do  {  *to++  =
                          *from++; case 1: do { *to++ = *from++;
                              while (--n>0);
                      }
                  }
           
                Зачем кто-то мог написать нечто похожее?
             11. (*2) Напишите функцию atoi(), которая получает строку, содер-
                жащую  цифры,  и  возвращает  соответствующее  int.  Например,
                atoi("123") - это 123.  Модифицируйте atoi() так, чтобы помимо
                обычной десятичной она обрабатывала еще восьмеричную  и  шест-
                надцатиричную записи С++.  Модифицируйте atoi() так, чтобы об-
                рабатывать  запись  символьной  константы.  Напишите   функцию
                itoa(),  которая  строит представление целого параметра в виде
                строки.
           
             12. (*2) Перепишите get_token() (#3.1.2),  чтобы она за один  раз
                читала строку в буфер,  а затем составляла лексемы, читая сим-
                волы из буфера.
           
             13. (*2) Добавьте в настольный калькулятор из #3.1 такие функции,
                как sqrt(),  log() и sin().  Подсказка: предопределите имена и
                вызывайте функции с помощью вектора указателей на функции.  Не
                забывайте проверять параметры в вызове функции.
           
             14. (*3)  Дайте  пользователю  возможность  определять  функции в
                настольном калькуляторе.  Подсказка:  определяйте функции  как
                последовательность действий,  прямо так, как их набрал пользо-
                ватель.  Такую последовательность можно хранить или  как  сим-
                вольную  строку,  или  как список лексем.  После этого,  когда
                функция вызывается, читайте и выполняйте эти действия. Если вы
                хотите,  чтобы пользовательская функция получала параметры, вы
                должны придумать форму записи этого.
           
             15. (*1.5) Преобразуйте настольный калькулятор так,  чтобы вместо
                статических  переменных name_string и number_value использова-
                лась структура символа symbol:
           
                  struct symbol { token_value tok; union {
                          double number_value; char* name_string;
                      };
                  };
           
             16. (*2.5) Напишите программу, которая выбрасывает комментарии из
                С++ программы. То есть, читает из cin, удаляет // и /* */ ком-
                ментарии  и  пишет результат в cout.  Не заботьтесь о приятном
                виде выходного текста (это могло бы быть другим, более сложным
                упражнением). Не беспокойтесь о правильности программ. Остере-
                гайтесь // и /* и */ внутри комментариев,  строк и  символьных
                констант.
           
             17. (*2) Посмотрите какие-нибудь программы,  чтобы понять принцип
                различных  стилей  комментирования  и  выравнивания,   которые
                используются на практике.
