Урок №40. Инкремент, декремент и побочные эффекты

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 135114

 ǀ   40 

На этом уроке мы рассмотрим, что такое инкремент и декремент в языке С++, а также разберемся с таким понятием, как «побочные эффекты».

Инкремент и декремент

Операции инкремента (увеличение на 1) и декремента (уменьшение на 1) переменных настолько используемые, что у них есть свои собственные операторы в языке C++. Кроме того, каждый из этих операторов имеет две версии применения: префикс и постфикс.

Оператор Символ Пример Операция
Префиксный инкремент (пре-инкремент) ++ ++x Инкремент x, затем вычисление x
Префиксный декремент (пре-декремент) −− −−x Декремент x, затем вычисление x
Постфиксный инкремент (пост-инкремент) ++ x++ Вычисление x, затем инкремент x
Постфиксный декремент (пост-декремент) −− x−− Вычисление x, затем декремент x

С операторами инкремента/декремента версии префикс всё просто. Значение переменной х сначала увеличивается/уменьшается, а затем уже вычисляется. Например:

А вот с операторами инкремента/декремента версии постфикс несколько сложнее. Компилятор создает временную копию переменной х, увеличивает или уменьшает оригинальный х (не копию), а затем возвращает копию. Только после возврата копия х удаляется. Например:

Рассмотрим вышеприведенный код детально. Во-первых, компилятор создает временную копию х, которая имеет то же значение, что и оригинал (5). Затем увеличивается первоначальный х с 5 до 6. После этого компилятор возвращает временную копию, значением которой является 5, и присваивает её переменной у. Только после этого копия x уничтожается. Следовательно, в вышеприведенном примере мы получим у = 5 и х = 6.

Вот еще один пример, показывающий разницу между версиями префикс и постфикс:

Результат выполнения программы:

5 5
6 4
6 4
6 4
7 3

В строке №7 переменные х и у увеличиваются/уменьшаются на единицу непосредственно перед обработкой компилятором, так что сразу выводятся их новые значения. А в строке №9 временные копии (х = 6 и у = 4) отправляются в cout, а только после этого исходные х и у увеличиваются/уменьшаются на единицу. Именно поэтому изменения значений переменных после выполнения операторов версии постфикс не видно до следующей строки.

Версия префикс увеличивает/уменьшает значения переменных перед обработкой компилятором, версия постфикс — после обработки компилятором.

Правило: Используйте префиксный инкремент и префиксный декремент вместо постфиксного инкремента и постфиксного декремента. Версии префикс не только более производительны, но и ошибок с ними (по статистике) меньше.

Побочные эффекты


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

В большинстве случаев побочные эффекты являются полезными:

В примере, приведенном выше, оператор присваивания имеет побочный эффект, который проявляется в изменении значения переменной х. Оператор ++ имеет побочный эффект инкремента переменной х. Вывод х имеет побочный эффект внесения изменений в консольное окно.

Также побочные эффекты могут приводить и к неожиданным результатам:

Язык C++ не определяет порядок, в котором вычисляются аргументы функции. Если левый аргумент будет вычисляться первым, то add(5, 6) и результат — 11. Если правый аргумент будет вычисляться первым, то add(6, 6) и результат — 12! А проблема то кроется в побочном эффекте одного из аргументов функции add().

Вот еще один пример:

Какой результат выполнения этой программы? Если инкремент переменной х выполняется до операции присваивания, то ответ — 1. Если же после операции присваивания, то ответ — 2.

Есть и другие случаи, в которых C++ не определяет порядок обработки данных, поэтому в разных компиляторах могут быть разные результаты. Но даже в тех случаях, когда C++ и уточняет порядок обработки данных, некоторые компиляторы все равно вычисляют переменные с побочными эффектами некорректно. Этого всего можно избежать, если использовать переменные с побочными эффектами не более одного раза в одном стейтменте.

Правило: Не используйте переменную с побочным эффектом больше одного раза в одном стейтменте.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (544 оценок, среднее: 4,89 из 5)
Загрузка...

Комментариев: 40

  1. Jenia:

    А почему копия переменной, созданная оператором пост-инкремента/декремента, не удаляется после выполнения объекта std::endl?

    1. CariaNet:

      endl переходит на новую строку, и он не относится к тому.

  2. Alex Road:

    Попробовал разобраться, как работают all_кременты.
    Найти логическое объяснения правилам крементирования в С++ мне не удалось до конца.
    Вероятно, правильно заметили, что постфиксами лучше вообще не пользоваться.

    Например, в данных примерах я представил все варианты умножения двух all_крементов:
    =====================================
    i = 4; a = (—i) * (—i); => a = 4
    i = 4; a = (—i) * (i—); => a = 6
    i = 4; a = (i—) * (—i); => a = 8
    i = 4; a = (i—) * (i—); => a = 12

    a = (++i) * (++i); => a = 36
    a = (++i) * (i++);
    a = (i++) * (++i);
    a = (i++) * (i++);

    a = (—i) * (++i); => a = 16
    a = (—i) * (i++);
    a = (i—) * (++i);
    a = (i—) * (i++);

    a = (++i) * (—i); => a = 16
    a = (++i) * (i—);
    a = (i++) * (—i);
    a = (i++) * (i—);
    ====================================

    Первый пример вопросов не вызывает:
    i = 4; a = (—i) * (—i); => a = 4

    Имеем здесь, как и в каждом примере по четыре действия:
    a = (—i) * (—i)

    Каждое действие я пронумеровал цифрами в скобках:
    a =(4) (—i)(1) *(3) (—i)(2)

    Получаем:
    (1) из первой i вычитаем единицу;
    (2) из второй i вычитаем единицу, имеем в итоге i = 2;
    (3) перемножаем две i; 2*2=4;
    (4) Приравниваем полученный результат к а.

    Что происходит дальше, для меня загадка.

    Например:
    i = 4; a = (i—) * (i—); => a = 12

    По правилам должно быть так:

    i = 4; a =(2) (i—)(3) *(1) (i—)(4); => a = 16

    Откуда берётся 12, совершенно не ясно.
    У нас здесь постфиксы, которые вообще не должны влиять не на что в данном перемножении двух i.

    1. Фото аватара Юрий:

      У Вас крутое оформление комментария)) Вот бы все так оформляли свои комментарии, как Вы 🙂

    2. Виктория:

      Это решение верно:
      i = 4; a = (i—) * (i—); => a = 12

      Т.к. первое значение "i" используется без уменьшения, во втором случае используется используется уже созданная изменённая копия "i"
      i = 4; a = (i—)(использует текущее 4) *(i—)(использует сохранённую изменённую копию, создавая новую); => a = 12

    3. Alex Road:

      — В книжке, с которой я начал своё обучение С++, отсутствовала тема приоритетов.
      Предыдущий урок расставил всё на свои места.
      Не знал, что приоритет любого декремента выше приоритета умножения.
      Вопрос закрыт.

      1. ArtemF:

        Всё зависит от компилятора и стандартов, а не того что написано в книгах или тут. В Visual Studio (у них свой компилятор) ответ 16. Видимо вы используете gcc и там 12. Вообще разницу и как это работает под капотом можно посмотреть на сайте онлайн-компиляторов.

        1. Alex Road:

          Если я Вас правильно понял, логика такая — прежде, чем использовать эти вещи, стоит проверить методом тыка в своём компиляторе первоначально?

        2. ArtemF:

          Лучше не использовать вообще, а если и использовать то понимать, что у кого-то это будет работать не так как ожидается. Да и вряд ли где-то можно встретить такой код x = (i—) * (i—) или x = x++, максимум на собеседовании.
          Насчет приоритетов не правильно написал. В этом примере в обоих случаях приоритет пост-декремента выше чем умножения как и написано в книгах.
          Дело в копии переменной, которая создается, когда выполняется декремент. В Visual Studio компилятор считает что умножение должно производится с оригиналом переменной, поэтому получается 16 и по факту нарушаются приоритеты. Хотя я сам поддерживаю порядок вычислений как написала Виктория и как это реализовано в gcc-компиляторе.
          Почему так я не знаю, может когда-нибудь это исправят.

  3. Егор:

    Постфикс круче!
    Поэтому язык называется С++, а не ++С!!!

    1. Константин:

      Вот представь: на автоматической винтовке счётчики патронов. Так вот префиксный счётчик считает отстрелянные патроны и после каждого выстрела в стволе ноль патронов, а постфиксный считает хитрее — вроде выстрел учтён, а в стволе находится очередной патрон. Так что если это не учитывать, то нарваться, однако, можно!

  4. ArtemF:

    Попытка разобраться почему x = x++ вызывал "undefined behavior".
    До с++17 компилятор VS выдавал такой код:
    x = x (1) — вычисляем правый операнд, присваивание здесь НЕ выполняется, мы просто ставим на место справа от равно 1.
    x = 1 — выполняем присваивание, значение "х" равно 1.
    Собственно на этом всё, согласно стандарту мы выполнили присваивание, но остался "побочный эффект" у оператора справа x++. И то когда выполнять пост-инкремент было не определено. VS компилятор делал это после т.е следующей была команда:
    x++ — и "х" равен 2, что и выводилось в консоль.
    В с++17 определили порядок: вычисление правого операнда далее вычисление выражения(т.е "побочного эффекта") и только потом операция присваивания:

    Теперь порядок выполнения операторов соответствует приоритету операций и сам инкремент выполняется правильно, через копию переменной. Как я понял так изначально было реализовано в компиляторе gcc, который выводил 1.

  5. Алексей:

    Почему ниже написанная программа выдаёт разный результат? У меня он такой:

    a = 17
    aa = 16

    Программа:

    1. Алексей:

      Охх, вроде разобрался, но не знаю почему такие различия (они будут ниже)
      Когда мы берём обычные числа, то все операции выполняются СЛЕВА на ПРАВО (и префиксный и постфиксный инкременты), а потом уже выполняется сумма, тоже слева направо. Но есть один нюанс. Та переменная (в вашем примере она первая) у которой префиксный инкр. она как бы является ссылкой. То есть она поменяется после изменения второй переменной (с постфиксным инкр). То есть будет в начале выглядеть так: 8 + b++, потому будет 9 + 8 (Тоесть b++ изменило и первую переменную), но при этом, что бы мы не делали, второе значение (8) не будет больше меняться, так как оно (значение) не связано с областью памяти b, в отличие от первой переменной.
      Во втором же случае, почему-то выполняются префиксный и постфиксный инкр. СПРАВА на ЛЕВО, а сумма работает так же, слева на право (в принципе логично). Что же получается в этом случае? Вторая переменная (b++) вычисляется первой, и у нас получается так: ++b + 7, в то же время b уже равна 8. Потом первая переменная, она уже получается равной 9. То есть 9 + 7 = 16, то, что у вас и получилось. Но я не понимаю, почему меняется порядок выполнения. То есть изначально всё как бы логично, а уже со своими классами как-то странно.
      P.S. это чисто моё недорасследование) Не надо слепо верить тому, что я тут написал (и вообще понятно ли что-то).

  6. Александр:

    Результат:

    2
    1
    2

    В чужом коде (который пока смотрю просто ради интереса) постоянно встречаю конструкции вида x++; По логике этого урока постфикс должен выводить 1, но нет же…

    1. Антон:

      В уроке всё верно.
      Вы сначала инициализируете переменную x значением 1

      потом пост инкрементируете переменную x

      после выводите переменную x c значением 2

      Давайте для примера возьмем 2 варианта.

      Во втором примере Вы выводите переменную y, которая себе присвоила на тот момент значение 1. В дальнейшем она так и остается единицей.
      А вот в первом примере вы выводите переменную x уже после того как сработал пост инкремент.

  7. Владислав:

    Вариант x = x++ выдаёт 1. Чисто логически это может выглядеть так:
    Сначала вычисляется выражение x, т.к была получена его копия. Затем идет инкремент, присваивается значение 2 для оригинала x. Но поскольку, выражение было вычислено с результатом 1, то после присвоения оригиналу 2 идёт присвоение вычисленного выражения.
    Ровно также, например, и для x = (x++)*3, выражение примет вид 1, затем оригиналу присвоится 2, затем выражение примет вид 1*3 = 3 и на выходе x будет иметь значение 3.

  8. ХейЛонг:

    х=х++ вообще не несёт адекватного смысла. инкременты декременты — это упрощённая запись присваивания. то бишь мы тут записали сразу два присваивания одной переменной, и пытаемся разобрать их порядок. мне кажется, что ответ будет одинаковый, потому что это выглядит как х = х = х = (х+1). инкременция происходит для переменной независимо от того, произойдёт она до или после первого присваивания.
    я кстати посчитал x = x++ + ++x — x— — —x; получилось нечто =)

  9. Алексей:

    Сделал такой пример:

    Ответ:

    2
    3

    Люди в комментариях немного разъяснили механизм работы постфиксного инкремента.

  10. Алексей:

    Попробовал видоизменить немного пример

    Ответ получил: 3

    Помоему это вообще дичь какая то.

    1. ХейЛонг:

      хочешь посмеяться? у меня тут ответ получается 4.
      сначала 1х3 = 3, а потом вместо увеличения икс просто добавляется к ответу. я подумал, что компилятор устал от моих экспериентов и начал меня троллить.

      1. Александр:

        Хаха, у меня тоже 4
        -_-

      2. Сергей:

        Думаю, что в выражении с постфиксным декрементом y = ((x++) * 3); выполнение всех операторов идет справа налево согласно ассоциативности, т.е. сначала 3*x при x=1, затем результат инкрементируется и присваивается y. Поэтому на выходе получается 4.

  11. Владимир:

    Непонятной является именно логика разработчиков С++ относительно фактической и задекларированной приоритетности постинкремента. С какой целью у него указывается почти наивысший теоретический приоритет, если по факту он самый низкий. Какое значение для нас имеет факт того, что компилятор ка бы уже прибавил еденицу, если мы можем это увидеть только после завершения других операций? Как нас это может согреть и для чего такой преподвыподверт с приоритетами? Почему не указать ему наинизший приоритет — и все стало бы вроде логично?

    1. Богдан:

      Полностью с вами согласен.

  12. Сергей:

    Для тех кто не понял постфиксный инкремент.

    Постфиксный инкремент увеличивает переменную сразу, но в качестве результата отдает ее предыдущее значение.

  13. Илья:

    Юрий, мне осталось непонятным, почему

    вызывает неоднозначность?

    Укажите, пожалуйста, на ошибку в моих рассуждениях:

    приоритет у постфиксного инкрементирования "++" выше,
    чем у оператора присваивания "=",
    следовательно, первым будет выполняться постинкремент;

    постинкремент, как Вы и писали:

    "Во-первых, компилятор создает временную копию х, которая имеет то же значение, что и оригинал (5). Затем увеличивается первоначальный х с 5 до 6. После компилятор вычисляет временную копию, значение которой — 5, и присваивает это значение переменной у. Затем копия удаляется."

    т.е. оператор постинкремента:
    1. делает резервную копию объекта x;
    2. производит инкрементирование переменной x;
    3. при выходе/завершении возвращает по значению резервную копию объекта x в точку вызова (выражение в правой части оператора присваивания);

    к моменту выполнения оператора присваивания
    слева от знака "=" находится уже инкрементированная переменная x
    справа — временный объект, являющийся копией ещё не инкрементированного значения переменной x

    т.е. оператор присваивания просто перезаписывает инкрементированное значение x неинкрементированным.

    1. Константин:

      Илья, ты жим штанги лёжа в зале когда-нибудь практиковал? Как ты считал повторения? Толкнул"ХУХ" в верхней точке произносишь "РАЗ", толкнул"ХУХ" в верхней точке произносишь "ДВА" и т.д. Правильно? Так вот это префиксный инкремент. А вот если ты толкнул"ХУХ"опустил и произнёс "РАЗ" — это постфиксный инкремент. Хочешь глубже понять эту тему — по-больше блинов на штангу надень!

      1. Фото аватара Юрий:

        Аахах, шедевр)

  14. Константин:

    Привет, Юр!
    Пожалуйста дай примеры "непоняток" помимо ин/декрементов. Спасибо!

  15. artem:

    Насчет правила не использовать постфиксный инкремент и постфиксный декремент: Когда изучал циклы for, while и do while везде использовали именно постфиксный инкремент и декремент, даже при разработке мини-игр.

    1. Фото аватара Юрий:

      То, как писать код — дело каждого лично. В уроках рассматриваются аспекты, которые следует знать и понимать.

  16. Иван:

    А приоритет у '++' выше ведь, чем у '='? Почему будет неоднозначность?

    1. Фото аватара Юрий:

      Да, приоритет у "++" выше, нежели в "=". И у всех компиляторах сначала выполнится операция инкремента, результат же зависит от того, в каком направлении будет выполняться следующая операция, т.е. операция присваивания. В случае с:

      Сначала выполняется операция инкремента, так как у неё приоритет выше — получим:

      И здесь уже важно то, в каком направлении будет выполняться операция присваивания, либо x++ (значение 2) будет присваиваться переменной х — результат 2, либо переменная х (значение 1) будет присваиваться x++ — результат 1. То есть, либо справа налево, либо слева направо. А это уже вопрос отдельно к каждому компилятору и его разработчику. Но операция инкремента (++) выполняется первой в любом случае.

      1. Сергей:

        Юрий, почему происходит нарушение "логики", если у нас программа выполняется сверху вниз, то сначала присваивается переменная, потом происходит инкремент, а потом она (переменная) выводится на экран? Разве на экран не должно выводиться инкрементированное число?

        1. Фото аватара Юрий:

          В статье об этом ведь рассказывается. Есть две версии инкремента: постфикс и префикс.

          В версии префикс число сразу увеличивается и выводиться уже увеличенное:

          В версии постфикс значение сначала присваивается, затем вычисляется (увеличивается):

          Видите плюсы? Они находятся в разных сторонах от переменной x — так можно легко вычислять результат (если плюсы находятся после переменной, то увеличиваться значение будет после выполнения операции присваивания и наоборот).

          Если бы версии постфикс и префикс вычислялись одинаково, то зачем тогда нужны были бы две версии? Сделали бы один общий инкремент/декремент, да й делов то. Но есть случаи, где нужно использовать версии постфикс, есть случаи, где нужно использовать версии префикс — поэтому есть разделение на версии.

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

      2. Владимир:

        Так вы же в 38 уроке сами указали, что операция присваивания имеет ассоциативность R->L. Следовательно, именно значение инкремента присвоится переменной x. Причём не говорилось, что такие приоритеты имеют какие-то определённые компиляторы, почему здесь есть вопрос к разработчику и компилятору — не понимаю. В том же уроке даже было задание 'В', где как раз и был случай с инкрементом, и ответ там был точно по такому же принципу.

      3. Sasha:

        В таблице по-моему был столбец ассоциативность. Для присваивания справа налево, почему же разные компиляторы видят это по-разному

  17. Anton:

    Странно всё, в первой половине текста рассказывается, что за чем вычисляется, а во второй уже опровержение в стиле "первая часть текста смысла не несёт, так как всё это великий китайский рандом, зависящий от компилятора". Этот урок закинул в дальний уголок памяти, так как пока всё равно в коде ничего сложнее простого повышения значения на единичку не использую.

    1. Фото аватара Юрий:

      В первой части урока объясняется "инкремент" и "декремент". Во второй части речь идёт о другом понятии — "побочные эффекты". Какое здесь опровержение и чего? Опровержение не выполнять инкремент и декремент или что? Есть префикс, есть постфикс, есть побочные эффекты. Китайского рандома нет.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *