Пост-инкремент и предварительный инкремент в цикле «for» производят одинаковый вывод

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

Вот код:

for(i=0; i<5; i++) { printf("%d", i); } for(i=0; i<5; ++i) { printf("%d", i); } 

Я получаю тот же вывод для обоих циклов for. Я что-то упускаю?

После оценки i++ или ++i новое значение i будет одинаковым в обоих случаях. Разница между пре-и пост-приращением возникает в результате оценки самого выражения.

++i увеличивает i и оценивает новое значение i .

i++ оценивает старое значение i и увеличивает i .

Причина, по которой это не имеет значения в цикле for, заключается в том, что stream управления работает примерно так:

  1. проверить условие
  2. если оно ложно, прекратите
  3. если это правда, выполните тело
  4. выполнить шаг инкрементации

Поскольку (1) и (4) развязаны, можно использовать до или после инкремента.

Ну, это просто. Вышеупомянутые for циклов семантически эквивалентны

 int i = 0; while(i < 5) { printf("%d", i); i++; } 

а также

 int i = 0; while(i < 5) { printf("%d", i); ++i; } 

Обратите внимание, что строки i++; и ++i; имеют одинаковую семантику ОТ ПЕРСПЕКТИВЫ ЭТОГО БЛОКА КОДА. Они оба оказывают одинаковое влияние на значение i (увеличивают его на единицу) и, следовательно, оказывают такое же влияние на поведение этих циклов.

Обратите внимание, что будет разница, если цикл был переписан как

 int i = 0; int j = i; while(j < 5) { printf("%d", i); j = ++i; } int i = 0; int j = i; while(j < 5) { printf("%d", i); j = i++; } 

Это связано с тем, что в первом блоке кода j видит значение i после инкремента ( i увеличивается в первом порядке или предварительно увеличивается, следовательно, имя), а во втором блоке кода j видит значение i до приращения.

Результат вашего кода будет таким же. Причина в том, что две операции инкремента можно рассматривать как два различных вызова функций. Обе функции вызывают прирост переменной, и только их возвращаемые значения различаются. В этом случае возвращаемое значение просто выбрасывается, а это означает, что различий в выходе нет.

Однако под капотом есть разница: пост-инкремент i++ должен создать временную переменную для хранения исходного значения i , затем выполняет инкремент и возвращает временную переменную. Pre-incrementation ++i не создает временную переменную. Конечно, любой приемлемый параметр оптимизации должен быть в состоянии оптимизировать это, когда объект является чем-то простым, как int , но помните, что операторы ++ перегружены в более сложных classах, таких как iteratorы. Поскольку у двух перегруженных методов могут быть разные операции (возможно, вы захотите вывести «Hey, я pre-incremented!» На stdout, например), компилятор не может определить, эквивалентны ли методы, когда возвращаемое значение не используется (в основном потому, что такой компилятор разрешит неразрешимую проблему с остановкой ), он должен использовать более дорогую версию после инкремента, если вы напишете myiterator++ .

Три причины, почему вы должны заранее увеличивать:

  1. Вам не придется думать о том, может ли переменная / объект иметь перегруженный метод пост-инкремента (например, в функции шаблона) и относиться к нему по-другому (или забыть относиться к нему по-разному).
  2. Последовательный код выглядит лучше.
  3. Когда кто-то спрашивает вас: «Почему вы заранее увеличиваете?» вы получите возможность научить их проблеме остановки и теоретическим пределам оптимизации компилятора . 🙂

Это один из моих любимых вопросов интервью. Сначала я объясню ответ, а потом расскажу вам, почему мне нравится этот вопрос.

Решение:

Ответ заключается в том, что оба fragmentа печатают цифры от 0 до 4 включительно. Это связано с тем, что цикл for() обычно эквивалентен циклу while() :

 for (INITIALIZER; CONDITION; OPERATION) { do_stuff(); } 

Можно написать:

 INITIALIZER; while(CONDITION) { do_stuff(); OPERATION; } 

Вы можете видеть, что ОПЕРАЦИЯ всегда выполняется в нижней части цикла. В этой форме должно быть ясно, что i++ и ++i будут иметь тот же эффект: они оба увеличивают i и игнорируют результат. Новое значение i не проверяется до начала следующей итерации в верхней части цикла.


Изменить : Спасибо Джейсону за указание, что for() эквивалентности for() для while() не выполняется, если цикл содержит управляющие операторы (такие как continue ), которые предотвратили бы выполнение операции в цикле while() . OPERATION всегда выполняется непосредственно перед следующей итерацией цикла for() .


Почему это вопрос хорошего интервью

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

Но удивительно (для меня), многие кандидаты говорят мне, что цикл с пост-приращением будет печатать цифры от 0 до 4, а цикл предварительного инкремента будет печатать от 0 до 5 или от 1 до 5. Обычно они объясняют разницу между до и после приращения правильно, но они неправильно понимают механику цикла for() .

В этом случае я прошу их переписать цикл, используя while() , и это действительно дает мне хорошее представление о их мысленных процессах. И именно поэтому я задаю вопрос в первую очередь: я хочу знать, как они подходят к проблеме, и как они действуют, когда я сомневаюсь в том, как работает их мир.

На данный момент большинство кандидатов осознают свою ошибку и находят правильный ответ. Но у меня был тот, кто настаивал, что его первоначальный ответ был прав, а затем изменил способ, которым он перевел for() на while() . Это сделало для увлекательного интервью, но мы не сделали предложение!

Надеюсь, это поможет!

Поскольку в любом случае приращение выполняется после тела цикла и, таким образом, не влияет на какие-либо вычисления цикла. Если компилятор является глупым, может быть немного менее эффективным использовать пост-инкремент (поскольку обычно ему необходимо сохранить копию предварительного значения для последующего использования), но я ожидал бы, что в этом случае любые различия будут оптимизированы.

Возможно, было бы полезно подумать о том, как цикл for реализован, в основном переведенный в набор заданий, тестов и инструкций ветвления. В псевдокоде предварительный приращение будет выглядеть так:

  set i = 0 test: if i >= 5 goto done call printf,"%d",i set i = i + 1 goto test done: nop 

Пост-инкремент имел бы как минимум еще один шаг, но было бы тривиально оптимизировать

  set i = 0 test: if i >= 5 goto done call printf,"%d",i set j = i // store value of i for later increment set i = j + 1 // oops, we're incrementing right-away goto test done: nop 

Если бы вы написали это так, это было бы важно:

 for(i=0; i<5; i=j++) { printf("%d",i); } 

Повторялся бы еще раз, если бы это было написано так:

 for(i=0; i<5; i=++j) { printf("%d",i); } 

Вы можете прочитать ответ Google для этого здесь: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preincrement_and_Predecrement

Итак, главное, какая разница для простого объекта, но для iteratorов и других объектов шаблона вы должны использовать preincrement.

Редакция:

Нет никакой разницы, потому что вы используете простой тип, поэтому никаких побочных эффектов и пост-или преинкрементов, выполняемых после тела цикла, так что никакого влияния на значение в теле цикла нет.

Вы можете проверить это с помощью такой петли:

 for (int i = 0; i < 5; cout << "we still not incremented here: " << i << endl, i++) { cout << "inside loop body: " << i << endl; } 

И i ++ и ++ i выполняется после того, как printf («% d», i) выполняется каждый раз, поэтому нет никакой разницы.

Да, вы получите точно такие же результаты для обоих. почему вы думаете, что они должны дать вам разные результаты?

Пост-инкремент или предварительный прирост имеют значение в таких ситуациях:

 int j = ++i; int k = i++; f(i++); g(++i); 

где вы предоставляете некоторую ценность, либо путем назначения, либо путем передачи аргумента. Вы не делаете ни того, ни другого. Он получает только прирост. Пост-и до этого не имеет смысла!

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

Существует разница, если:

 int main() { for(int i(0); i<2; printf("i = post increment in loop %d\n", i++)) { cout << "inside post incement = " << i << endl; } for(int i(0); i<2; printf("i = pre increment in loop %d\n",++i)) { cout << "inside pre incement = " << i << endl; } return 0; } 

Результат:

внутри postmentment = 0

i = пост-приращение в цикле 0

внутри postmentment = 1

i = пост-приращение в цикле 1

Второй для цикла:

внутри prementcement = 0

i = предварительный прирост в цикле 1

внутри prementcement = 1

i = предварительный прирост в цикле 2

Составители

 for (a; b; c) { ... } 

в

 a; while(b) { ... end: c; } в a; while(b) { ... end: c; } 

Так что в вашем случае (post / pre-increment) это не имеет значения.

EDIT: продолжение просто заменяется goto end;

Давайте будем гением компьютера.