在“for”循环中,后增和前增产生相同的输出结果

245
以下这两个 for 循环产生相同的结果,尽管一个使用后增量运算符(post increment),另一个则使用前增量运算符(pre-increment)。
以下是代码:
for(i=0; i<5; i++) {
    printf("%d", i);
}

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

我使用两个“for”循环得到相同的输出,我错过了什么吗?


我查看了几个链接,但没有找到我要找的答案。简短的答案是“序列点”。引用C11草案附录C序列点的部分内容:“- 在完整表达式的评估和下一个要评估的完整表达式之间。以下是完整表达式:……for语句的每个(可选)表达式(6.8.5.3);返回语句中的(可选)表达式(6.8.6.4)。这意味着在for循环的每个表达式后都有序列点。因此,在for循环的第三个表达式中执行++i或i++或i+=1或i=i+1并不重要。 - ZeZNiQ
12个回答

392

在评估i++++i之后,i的新值在两种情况下都相同。 前增和后增的区别在于对表达式本身的评估结果。

++i递增i并计算出i的新值。

i++计算出i的旧值,并递增i

这在for循环中并不重要的原因是控制流大致按照以下方式工作:

  1. 测试条件
  2. 如果为false,则终止
  3. 如果为true,则执行主体
  4. 执行递增步骤

由于(1)和(4)是解耦的,因此可以使用前增或后增。


79
考虑到i++需要在自增后记住旧值,我认为使用++i可能更短(可能少1-2条指令)。 - danben
30
未使用的值应该被优化掉,对吧? - Jamin Grey
16
如果没有使用 i++ 的理由,采用 ++i 习惯有什么不好呢? - Azendale
15
Azendale说得对。默认使用后增/减运算符是许多人具有的一种非常不幸的习惯。只有在明确且可辩护的情况下才应该使用后缀。当然,在任何非微不足道的编译器中,未求值的基元可能不会有任何区别,但这并不意味着后缀应该成为默认选择;这样的习惯最终只会带来麻烦。 - underscore_d
30
如果 C++ 被称为 ++C,那么很可能默认的习惯会是 ++i :) - marco
显示剩余13条评论

130

很简单。上述的for循环在语义上等同于

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

int i = 0;
while(i < 5) {
    printf("%d", i);
    ++i;
}
注意,就这个代码块而言,i++;++i; 这两行的语义相同。它们对 i 的值都有相同的影响(增加1),因此对这些循环的行为产生相同的影响。
请注意,如果将循环改写为:
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 增加前的值。

109
你的代码结果将是相同的。原因在于这两个递增操作可以看作是两个不同的函数调用。两个函数都会导致变量递增,只有它们的返回值不同。在这种情况下,返回值被抛弃,这意味着输出中没有可区分的差异。

然而,在编程中,底层实现有所区别:后缀自增运算符i++需要创建一个临时变量来存储i的原始值,然后执行自增操作并返回该临时变量。前缀自增运算符++i不会创建临时变量。当对象是简单类型(如int)时,任何优化设置都应该能够将其优化掉,但请记住,像迭代器这样更复杂的类中,++运算符被重载了。由于两个重载方法可能有不同的操作(例如,其中一个可能要在stdout输出“嘿,我被前缀自增了!”),如果返回值没有被使用,编译器无法确定这两种方法是否相等(基本上因为这样的编译器将解决无法解决的停机问题),它需要使用更昂贵的后缀自增版本,如果你写myiterator++

以下是三个使用前缀自增运算符的理由:

  1. 你不必考虑变量/对象是否具有重载的后置增量方法(例如在模板函数中),也不必将其视为不同的处理方式(或忘记将其视为不同的处理方式)。
  2. 一致的代码看起来更好。
  3. 当有人问你“为什么要使用前置递增?”时,你将有机会教他们关于停机问题和编译器优化的理论限制。 :)

我认为你错了!++i和i++唯一的区别在于执行顺序。在一个情况下,你会 inc *i push *i而在另一种情况下,你会 push *i inc *i我不认为这是一种优化。 - G. Allen Morris III
21
他说的没错。如果你不使用副本,后增基元可以很容易地被编译器优化掉。但他最后暗示的重点是,在迭代器上进行后增操作实际上是一个方法调用,并创建了一个全新的迭代器副本。这些不能由编译器优化掉,因为构造函数和函数调用可能会产生无法跟踪的副作用,因此编译器不能假定它们不是关键的。 - Jherico
在一个情况下,你会使用inc *i push *i,而在另一个情况下,你会使用push *i inc *i。我不认为这是一种优化的做法。 - MQDuck
3
他是错误的:我在编译器资源网站https://godbolt.org/上尝试了前置和后置递增操作符,无论你如何使用这两种递增方式,都会获得完全相同的汇编代码。对于C语言,你只是描述了你希望编译器执行的操作,如果这些操作达到同样的效果,那么通常会得到相同的输出。现代编译器非常聪明,前置和后置递增几乎从不产生任何差别,如果你不相信,请在编译器资源网站上验证一下! - Will
1
当然,但是你是否使用基本类型或一些重载增量的更复杂类型进行了检查? - Karlovsky120
他并没有错,显然只会读一半... - peter

32

这是我最喜欢的面试题之一。我将先解释答案,然后告诉您为什么我喜欢这个问题。

解决方案:

答案是两个片段都会打印从0到4(包括4)的数字。这是因为for()循环通常等同于while()循环:

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

可以这样写:

INITIALIZER;
while(CONDITION) {
    do_stuff();
    OPERATION;
}
你可以看到,在循环结构的底部总是会执行 OPERATION 操作。这种形式下,i++++i 会产生相同的效果:它们都会递增 i 并忽略结果。新的 i 值直到下一次迭代开始时才被测试,即在循环顶部。


编辑: 感谢 Jason 指出如果循环包含控制语句(如 continue)会阻止在 while() 循环中执行 OPERATION,因此此处的for()while() 不等价。在 for() 循环中,在下一次迭代之前始终执行 OPERATION


为什么这是一个好的面试题目

首先,如果候选人能立即正确回答,只需要花费一两分钟,我们就可以进入下一个问题。

但令我惊讶的是,许多候选人告诉我,使用后置增量的循环将打印从 0 到 4 的数字,而使用前置增量的循环将打印 0 到 5 或 1 到 5。他们通常正确地解释了前缀和后缀递增之间的区别,但是他们误解了 for() 循环的机制。

在这种情况下,我会要求他们使用 while() 重写循环,这真的可以让我了解到他们的思考过程。这也是我问这个问题的原因:我想了解他们解决问题的方式,以及当我对他们的世界观提出质疑时,他们如何继续。

此时,大多数候选人会意识到他们的错误并找到正确的答案。但是我遇到了一个坚持认为自己最初的答案是正确的候选人,然后改变了他将 for() 转换为 while() 的方式。虽然这次面试十分有趣,但我们没有给他发 Offer!

希望这能帮到你!


27
你应该重新考虑提出这个面试问题,因为你犯了一个严重但常见的错误。通常情况下,一个 for 循环不能按照你指定的方式被重写。例如看看 for(int i = 0; i < 42; i++) { printf("%d", i); continue; } 这个例子。你声称它在语义上等同于 int i = 0; while(i < 42) { printf("%d", i); continue; i++; },但这显然是错误的。 - jason
9
@Jason:今天我学到了一个我之前没有考虑过的边缘情况!在近十年中,我从来没有遇到过一个候选人能够认出这个情况。如果我继续使用这个问题,因为它有我提到的那些价值,我会根据上面的修改重新表述它。如果一个候选人立即给出正确答案,我会问是否有任何例外。 :-)你的明确反例加一分。谢谢! - Adam Liss
1
@jdehaan:我没有说通用的for循环等同于你和@Adam Liss提供的通用的while循环。我是说在这个特定的例子中,OP的for循环和我给出的while循环之间的语义是相同的。一般来说,从for循环到while循环的朴素翻译是有问题的,这就是你陷入麻烦的原因。顺便说一句,注意编译器甚至不需要将OP的for循环转换为任何类型的循环;例如,编译器可以完全展开循环。 - jason
1
@Jason:是的,我通常从看起来像简单的面试问题开始,然后根据候选人的回答“深挖”一下。直到现在,我的大部分经验都是与一个小型工程公司合作的,他们倾向于避免新毕业生,更喜欢电气工程师而不是更专注于计算机科学的开发人员。(这并不是我的判断,只是事实如此。)无论如何,我很高兴我的回答引发了这个讨论,并感谢你的想法。 - Adam Liss
如果你把它看作是一种对抗或者欺凌的过程,无论你是面试官还是应聘者,那么对于我需要填补的职位来说,这确实是适得其反的。另一方面,如果这是一个合作友好的谈话,反映了实际的工作环境,那么你们每个人都可以学到很多关于如何共同工作的东西。作为面试官,我的工作之一就是帮助候选人放松心情,专注于任务,并给他们成功的机会。我想知道他们能完成多少工作,而不是想要绊倒他们。 - Adam Liss
显示剩余4条评论

9
无论哪种情况,增量都是在循环体之后执行的,因此不会影响循环中的任何计算。如果编译器比较愚蠢,使用后置增量可能会略微降低效率(因为通常需要保留先前的值供以后使用),但在这种情况下我预期任何差异都会被优化掉。
想象一下 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

6
如果你写成这样,那么就会有所影响:
for(i=0; i<5; i=j++) {
    printf("%d",i);
}

如果像这样编写代码,将会比下面的代码多迭代一次:

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

3

在每次执行printf("%d", i)之后,i++和++i都会被执行,因此它们没有区别。


3
您可以在此处阅读Google的答案: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preincrement_and_Predecrement 因此,主要问题是,在简单对象上没有区别,但对于迭代器和其他模板对象,您应该使用预增量。
编辑:
没有区别,因为您使用简单类型,所以没有副作用,并且后置或前置增量在循环体之后执行,因此不会影响循环体中的值。
您可以使用以下循环进行检查:
for (int i = 0; i < 5; cout << "we still not incremented here: " << i << endl, i++)
{
    cout << "inside loop body: " << i << endl;
}

1
这并没有回答为什么输出结果是相同的问题。 - Bo Persson
@Bo Persson,公平起见,您应该不仅仅为我的答案打分。我认为您真的很粗鲁。 - Yola
1
也许我是,但其他答案已经两年了,而且实际上试图回答“我想知道为什么没有区别”。 - Bo Persson
@Bo Persson,刚发现并回答了,没提到日期。 - Yola

2
在for循环中的第三个语句只会被执行,但它的计算值会被丢弃而不被处理。
当计算值被丢弃时,前置和后置增量是相等的。
只有在它们的值被使用时才会有所不同。

0

是的,你会得到完全相同的输出。为什么你认为它们应该给你不同的输出呢?

后增量或前增量在这种情况下很重要:

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

在编程中,你可以通过赋值或传递参数来提供一些值。但是在你的for循环中,你既不赋值也不传递参数,只对它进行递增操作。因此后置和前置这两种运算在这里没有任何意义!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接