|
Часть 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) Посмотрите какие-нибудь программы, чтобы понять принцип
различных стилей комментирования и выравнивания, которые
используются на практике.
|
|