Таблица 2.1. Приоритеты и очередность вычислений операторов
Операторы | Выполняются |
---|---|
() [] -› . | слева направо |
! ~ ++ -- + - * & (type) sizeof | справа налево |
* / % | слева направо |
+ - | слева направо |
‹‹ ›› | слева направо |
‹ ‹= › ›= | слева направо |
== != | слева направо |
& | слева направо |
^ | слева направо |
| | слева направо |
&& | слева направо |
|| | слева направо |
?: | справа налево |
= += -= *= /= %= &= ^= |= ‹‹= ››= | справа налево |
, | слева направо |
Примечание. Унарные операторы +, -, * и & имеют более высокий приоритет, чем те же бинарные операторы.
Заметим, что приоритеты побитовых операторов &, ^ и | ниже, чем приоритет == и !=, из-за чего в побитовых проверках, таких как
if ((x & MASK) == 0)…
чтобы получить правильный результат, приходится использовать скобки. Си подобно многим языкам не фиксирует очередность вычисления операндов оператора (за исключением &&, ||, ?: и ,). Например, в инструкции вида
x = f() + g();
f может быть вычислена раньше g или наоборот. Из этого следует, что если одна из функций изменяет значение переменной, от которой зависит другая функция, то помещаемый в x результат может зависеть от очередности вычислений. Чтобы обеспечить нужную последовательность вычислений, промежуточные результаты можно запоминать во временных переменных.
Очередность вычисления аргументов функции также не определена, поэтому на разных компиляторах
printf("%d %d\n", ++n, power(2, n)); /* НЕВЕРНО*/
может давать несовпадающие результаты. Результат вызова функции зависит от того, когда компилятор сгенерирует команды увеличения n - до или после обращения к power. Чтобы обезопасить себя от возможного побочного эффекта, достаточно написать
++n;
printf("%d %d\n", n, power(2, n));
Обращения к функциям, вложенные присвоения, инкрементные и декрементные операторы дают "побочный эффект", проявляющийся в том, что при вычислении выражения значения некоторых переменных изменяются. В любом выражении с побочным эффектом может быть скрыта трудно просматриваемая зависимость результата выражения от очередности изменения значений переменных, входящих в выражение. В такой, например, типично неприятной ситуации
a[i] = i++; /* I.B.: doubtful example */
возникает вопрос: массив a индексируется старым или измененным значением i? Компиляторы могут по-разному генерировать программу, что проявится в интерпретации данной записи. Стандарт сознательно устроен так, что большинство подобных вопросов оставлено на усмотрение компиляторов, так как лучший порядок вычислений определяется архитектурой машины. Стандартом только гарантируется, что все побочные эффекты при вычислении аргументов проявятся перед входом в функцию. Правда, в примере с printf это нам не поможет.
Мораль такова: писать программы, зависящие от очередности вычислений, - плохая практика, какой бы язык вы ни использовали. Естественно, надо знать, чего следует избегать, но если вы не знаете, как образуются побочные эффекты на разных машинах, то лучше и не рассчитывать выиграть на особенностях частной реализации.
Глава 3. Управление
Порядок, в котором выполняются вычисления, определяется инструкциями управления. Мы уже встречались с наиболее распространенными управляющими конструкциями такого рода в предыдущих примерах; здесь мы завершим их список и более точно определим рассмотренные ранее.
3.1 Инструкции и блоки
Выражение, скажем x = 0, или i++, или printf(:), становится инструкцией, если в конце его поставить точку с запятой, например:
x = 0;
i++;
printf(…);
В Си точка с запятой является заключающим символом инструкции, а не разделителем, как в языке Паскаль.
Фигурные скобки { и } используются для объединения объявлений и инструкций в составную инструкцию, или блок, чтобы с точки зрения синтаксиса эта новая конструкция воспринималась как одна инструкция. Фигурные скобки, обрамляющие группу инструкций, образующих тело функции, - это один пример; второй пример - это скобки, объединяющие инструкции, помещенные после if, else, while или for. (Переменные могут быть объявлены внутри любого блока, об этом разговор пойдет в главе 4.) После правой закрывающей фигурной скобки в конце блока точка с запятой не ставится.
3.2 Конструкция if-else
Инструкция if-else используется для принятия решения. Формально ее синтаксисом является:
if (выражение)
инструкция1
else
инструкция2
причем else-часть может и отсутствовать. Сначала вычисляется выражение, и, если оно истинно (т. е. отлично от нуля), выполняется инструкция1. Если выражение ложно (т. е. его значение равно нулю) и существует else-часть, то выполняется инструкция2.
Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Так, запись
if (выражение)
короче, чем
if (выражение != 0 )
Иногда такие сокращения естественны и ясны, в других случаях, наоборот, затрудняют понимание программы.
Отсутствие else-части в одной из вложенных друг в друга if-конструкций может привести к неоднозначному толкованию записи. Эту неоднозначность разрешают тем, что else связывают с ближайшим if, у которого нет своего else. Например, в