ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



 

Часть 5

                              Глава 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() из  обеспечивает  стандартную
           проверку на то,  является ли символ пропуском (#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 - это стандартный  про-
           пусковый объект, описанный в ; он используется только для
           сброса пропуска. Ошибка во вводе или конец ввода не будут обнаруже-
           ны до следующего обращения к 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() заданы  в  ;  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> <<
           
           применяются к целым, то есть к объектам типа char, short, int, long
           и их unsigned аналогам, результаты тоже целые.
           
             Одно из стандартных применений побитовых  логических  операций  -
           реализация маленького множества (вектор битов).  В этом случае каж-
           дый бит беззнакового целого представляет  один  член  множества,  а
           число членов ограничено числом битов. Бинарная операция & интерпре-
           тируется как пересечение,  | как объединение, а ^ как разность. Для
           наименования  членов такого множества можно использовать перечисле-
           ние. Вот маленький пример, заимствованный из реализации (не пользо-
           вательского интерфейса) :
           
             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)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 
           
             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 
           
             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 && 1count) // ...
           
           вначале проверяет,  является ли p не нулем,  и только если это так,
           то проверяет 1count.
           
             Некоторые простые  операторы  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; im
                  *p.m
                  *a[i]
           
             7. (*2) Напишите  функции:  strlen(),  которая  возвращает  длину
                строки,  strcpy(),  которая  копирует одну строку в другую,  и
                strcmp(),  которая сравнивает две строки.  Разберитесь,  какие
                должны  быть  типы параметров и типы возвращаемых значений,  а
                потом сравните их со стандартными версиями,  которые описаны в
                 и в вашем руководстве.
           
             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) Посмотрите какие-нибудь программы,  чтобы понять принцип
                различных  стилей  комментирования  и  выравнивания,   которые
                используются на практике.


Яндекс цитирования