Синтаксис языка С++ перегружен скобками, и разнообразие их применений
способно сбить с толку. Они выделяют фактические параметры при
вызове функций, имена типов, задающих функции, а также служат для
разрешения конфликтов между операциями с одинаковым приоритетом.
К счастью, последнее встречается не слишком часто, поскольку приоритеты
и порядок применения операций определены так, чтобы выражения вычислялись
"естественным образом" (т.е. наиболее распространенным образом).
Например, выражение
if (i<=0 || maxозначает следующее: "Если i меньше или равно нулю, или если max меньше i".
То есть, оно эквивалентно
if ( (i<=0) || (maxно не эквивалентно допустимому, хотя и бессмысленному выражению
if (i <= (0||max) < i) // ...
Тем не менее, если программист не уверен в указанных правилах,
следует использовать скобки, причем некоторые предпочитают для
надежности писать более длинные и менее элегантные выражения, как:
if ( (i<=0) || (maxПри усложнении подвыражений скобки используются чаще. Не надо, однако,
забывать, что сложные выражения являются источником ошибок. Поэтому,
если у вас появится ощущение, что в этом выражении нужны скобки,
лучше разбейте его на части и введите дополнительную переменную.
Бывают случаи, когда приоритеты операций не приводят к "естественному"
порядку вычислений. Например, в выражении
if (i&mask == 0) // ловушка! & применяется после ==
не происходит маскирование i (i&mask), а затем проверка результата
на 0. Поскольку у == приоритет выше, чем у &, это выражение эквивалентно
i&(mask==0). В этом случае скобки играют важную роль:
if ((i&mask) == 0) // ...
Имеет смысл привести еще одно выражение, которое вычисляется
совсем не так, как мог бы ожидать неискушенный пользователь:
if (0 <= a <= 99) // ...
Оно допустимо, но интерпретируется как (0<=a)<=99, и результат первого
сравнения равен или 0, или 1, но не значению a (если, конечно,
a не есть 1). Проверить, попадает ли a в диапазон 0...99, можно так:
if (0<=a && a<=99) // ...
Среди новичков распространена ошибка, когда в условии вместо ==
(равно) используют = (присвоить):
if (a = 7) // ошибка: присваивание константы в условии
// ...
Она вполне объяснима, поскольку в большинстве языков "=" означает "равно".
Для транслятора не составит труда сообщать об ошибках подобного рода.
3.2.2 Порядок вычислений
Порядок вычисления подвыражений, входящих в выражение, не всегда
определен. Например:
int i = 1;
v[i] = i++;
Здесь выражение может вычисляться или как v[1]=1, или как v[2]=1.
Если нет ограничений на порядок вычисления подвыражений, то транслятор
получает возможность создавать более оптимальный код. Транслятору
следовало бы предупреждать о двусмысленных выражениях, но к сожалению
большинство из них не делает этого.
Для операций
&& || ,
гарантируется, что их левый операнд вычисляется раньше правого операнда.
Например, в выражении b=(a=2,a+1) b присвоится значение 3. Пример
операции || был дан в $$3.2.1, а пример операции && есть в $$3.3.1.
Отметим, что операция запятая отличается по смыслу от той запятой, которая
используется для разделения параметров при вызове функций. Пусть есть
выражения:
f1(v[i],i++); // два параметра
f2( (v[i],i++) ) // один параметр
Вызов функции f1 происходит с двумя параметрами: v[i] и i++, но
порядок вычисления выражений параметров неопределен. Зависимость
вычисления значений фактических параметров от порядка вычислений
- далеко не лучший стиль программирования. К тому же программа
становится непереносимой.
Вызов f2 происходит с одним параметром, являющимся выражением,
содержащим операцию запятая: (v[i], i++). Оно эквивалентно i++.
Скобки могут принудительно задать порядок вычисления. Например,
a*(b/c) может вычисляться как (a*b)/c (если только пользователь
видит в этом какое-то различие). Заметим, что для значений с плавающей
точкой результаты вычисления выражений a*(b/c) и (a*b)/ могут
различаться весьма значительно.
3.2.3 Инкремент и декремент
Операция ++ явно задает инкремент в отличие от неявного его задания
с помощью сложения и присваивания. По определению ++lvalue означает
lvalue+=1, что, в свою очередь означает lvalue=lvalue+1 при условии,
что содержимое lvalue не вызывает побочных эффектов. Выражение,
обозначающее операнд инкремента, вычисляется только один раз. Аналогично
обозначается операция декремента (--). Операции ++ и -- могут
использоваться как префиксные и постфиксные операции. Значением ++x
является новое (т. е. увеличенное на 1) значение x. Например, y=++x
эквивалентно y=(x+=1). Напротив, значение x++ равно прежнему значению x.
Например, y=x++ эквивалентно y=(t=x,x+=1,t), где t - переменная того
же типа, что и x.
Напомним, что операции инкремента и декремента указателя
эквивалентны сложению 1 с указателем или вычитанию 1 из указателя, причем
вычисление происходит в элементах массива, на который настроен
указатель. Так, результатом p++ будет указатель на следующий элемент.
Для указателя p типа T* следующее соотношение верно по определению:
long(p+1) == long(p) + sizeof(T);
Чаще всего операции инкремента и декремента используются для
изменения переменных в цикле. Например, копирование строки,
оканчивающейся нулевым символом, задается следующим образом:
inline void cpy(char* p, const char* q)
{
while (*p++ = *q++) ;
}
Язык С++ (подобно С) имеет как сторонников, так и противников именно
из-за такого сжатого, использующего сложные выражения стиля
программирования. Оператор
while (*p++ = *q++) ;
вероятнее всего, покажется невразумительным для незнакомых с С.
Имеет смысл повнимательнее посмотреть на такие конструкции, поскольку
для C и C++ они не является редкостью.
Сначала рассмотрим более традиционный способ копирования массива
символов:
int length = strlen(q)
for (int i = 0; i<=length; i++) p[i] = q[i];
Это неэффективное решение: строка оканчивается нулем; единственный
способ найти ее длину - это прочитать ее всю до нулевого символа;
в результате строка читается и для установления ее длины, и для
копирования, то есть дважды. Поэтому попробуем такой вариант:
for (int i = 0; q[i] !=0 ; i++) p[i] = q[i];
p[i] = 0; // запись нулевого символа
Поскольку p и q - указатели, можно обойтись без переменной i,
используемой для индексации:
while (*q !=0) {
*p = *q;
p++; // указатель на следующий символ
q++; // указатель на следующий символ
}
*p = 0; // запись нулевого символа
Поскольку операция постфиксного инкремента позволяет сначала использовать
значение, а затем уже увеличить его, можно переписать цикл так:
while (*q != 0) {
*p++ = *q++;
}
*p = 0; // запись нулевого символа
Отметим, что результат выражения *p++ = *q++ равен *q. Следовательно,
можно переписать наш пример и так:
while ((*p++ = *q++) != 0) { }
В этом варианте учитывается, что *q равно нулю только тогда, когда
*q уже скопировано в *p, поэтому можно исключить завершающее
присваивание нулевого символа. Наконец, можно еще более сократить
запись этого примера, если учесть, что пустой блок не нужен, а
операция "!= 0" избыточна, т.к. результат условного выражения и так
всегда сравнивается с нулем. В результате мы приходим к
первоначальному варианту, который вызывал недоумение:
while (*p++ = *q++) ;
Неужели этот вариант труднее понять, чем приведенные выше? Только
неопытным программистам на С++ или С! Будет ли последний вариант
наиболее эффективным по затратам времени и памяти? Если не считать
первого варианта с функцией strlen(), то это неочевидно. Какой из
вариантов окажется эффективнее, определяется как спецификой системы
команд, так и возможностями транслятора. Наиболее эффективный алгоритм
копирования для вашей машины можно найти в стандартной функции копирования
строк из файла :
int strcpy(char*, const char*);
Достарыңызбен бөлісу: |