У операторов могут быть побочные эффекты. Эти побочные эффекты могут привести к неожиданным результатам вычислений; причем возможны ситуации, когда результаты будут изменяться в зависимости от компилятора и используемых опций (неопределенное поведение). Что делать, чтобы избежать этой проблемы?
Оператор можно себе представить как функцию от операндов. Большинство операторов не изменяет свои операнды, но некоторые, такие как инкремент/декремент (++/--) и присваивание (=), делает это. Про такие операторы говорят, что они обладают побочными эффектами. И если побочный эффект оператора присваивания, заключающийся собственно в присваивании, обычно полезен -- ради него мы этот оператор и используем, то с инкрементом/декрементом дело обстоит не так просто.
Последовательность выполнения операторов определяется их приоритетом. Рассмотрим код:
int a = 1, b = 1, c = 1, d;
d = a*b + b*c;
Оба умножения выполняются перед сложением, но что будет выполнено раньше -- a*b
или b*c
? В данном случае это не имеет значения, поскольку у оператора умножения нет побочных эффектов.
Однако, когда в выражении используются операторы с побочными эффектами, порядок выполнения может повлиять на результат.
int a = 1, b = 1, c;
c = b + b++;
Теперь, если b
выполняется перед b++
, то c
станет равно 2
. Если же, напротив, b++
выполняется перед b
, то c = 3
.
Очередность выполнения операторов, или как ее еще называют, ассоциативность, также известна. Тогда в чем проблема? Смотрим в таблицу, и получаем, что в последнем примере b++
выполняется раньше, в результате c = 3
.
Дело в том, что стандарт С не предписывает какого-либо определенного порядка выполнения операндов (например, слева направо). То есть то, что вы записали выражение именно так, не гарантирует, что именно так его и "прочтет" компилятор. Это позволяет компилятору выбрать тот или иной порядок выполнения и получить более быстрый машинный код.
Вместо заданной очередности выполнения операторов, стандарт вводит понятие точки следования (sequence point). Здесь объясняется, что это такое, и какие выражения допустимы с точки зрения стандарта.
Например, распространенные на собеседованиях задачи из серии "что получится в результате..."
int a = 10, b;
b=a++ + ++a;
printf("%d, %d, %d, %d", b ,a++, a, ++a);
и
int x = 2;
x += x++ + ++x;
printf("%d\n", x);
приводят к неопределённому поведению (undefined behavior), поскольку в них переменная (a
и x
соответственно) изменяется более одного раза в промежутке между двумя точками следования. В этом случае результат выполнения программы зависит от массы вещей, в частности, от выбранного компилятора и используемых при компиляции опций.
Следует отметить, что хороший компилятор в случае неопределенного поведения должен выдать предупреждение.
Рассмотрим еще один пример (из M. Уэйт, С. Прата, Д. Мартин "Язык Си"):
while (num < 21)
{
printf("%10d, %10d\n", num, num*num++);
}
Здесь в функции printf()
вычисление последнего аргумента может выполниться сначала, и приращение переменной num
произойдет до того, как будет определен первый аргумент. Поэтому, если до входа в printf()
num
был равен, допустим, 5, то вместо строки
5, 25
будет напечатано
6, 25
Стандарт С предоставляют компилятору возможность выбрать, какой аргумент функции вычислять первым.
Избежать всех этих трудностей достаточно просто:
- Не применяйте операции инкремента/декремента к переменной, которая входит в выражение более одного раза.
- Не применяйте операции инкремента/декремента к переменной, присутствующей в более чем одном аргументе функции.
Инкремент/декремент удобно использовать в заголовке цикла или как самостоятельный оператор (не входящий в состав других операторов). В других случаях лучше использовать +=
/-=
.
Что же касается задач с собеседований, то, если порядок выполнения выражений совпадает с записанным, в соответствии с очередностью, получим
22, 13, 13, 13
и
10
Комментарии
comments powered by Disqus