Date Редакция Категория comp Теги C

У операторов могут быть побочные эффекты. Эти побочные эффекты могут привести к неожиданным результатам вычислений; причем возможны ситуации, когда результаты будут изменяться в зависимости от компилятора и используемых опций (неопределенное поведение). Что делать, чтобы избежать этой проблемы?

Оператор можно себе представить как функцию от операндов. Большинство операторов не изменяет свои операнды, но некоторые, такие как инкремент/декремент (++/--) и присваивание (=), делает это. Про такие операторы говорят, что они обладают побочными эффектами. И если побочный эффект оператора присваивания, заключающийся собственно в присваивании, обычно полезен -- ради него мы этот оператор и используем, то с инкрементом/декрементом дело обстоит не так просто.

Последовательность выполнения операторов определяется их приоритетом. Рассмотрим код:

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

Стандарт С предоставляют компилятору возможность выбрать, какой аргумент функции вычислять первым.

Избежать всех этих трудностей достаточно просто:

  1. Не применяйте операции инкремента/декремента к переменной, которая входит в выражение более одного раза.
  2. Не применяйте операции инкремента/декремента к переменной, присутствующей в более чем одном аргументе функции.

Инкремент/декремент удобно использовать в заголовке цикла или как самостоятельный оператор (не входящий в состав других операторов). В других случаях лучше использовать +=/-=.

Что же касается задач с собеседований, то, если порядок выполнения выражений совпадает с записанным, в соответствии с очередностью, получим

22, 13, 13, 13

и

10



Комментарии

comments powered by Disqus