【问题标题】:Post-increment and pre-increment within a 'for' loop produce same output [duplicate]“for”循环中的后增量和前增量产生相同的输出[重复]
【发布时间】:2011-06-10 00:33:43
【问题描述】:

以下 for 循环产生相同的结果,即使一个使用后增量,另一个使用前增量。

代码如下:

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);return 语句 (6.8.6.4) 中的(可选)表达式。”。这意味着在每个表达式之后都有 for 循环中的序列点。因此,在 for 循环的第三个表达式中执行 ++i 或 i++ 或 i+=1 或 i=i+1 都没有关系。

标签: c++ c for-loop operator-precedence


【解决方案1】:

是的,您将获得完全相同的输出。为什么你认为他们应该给你不同的输出?

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

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

您通过分配或传递参数来提供一些值。你在 for 循环中都不做。它只会增加。 Post- 和 pre- 在那里没有意义!

【讨论】:

    【解决方案2】:

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

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

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

    这在 for 循环中无关紧要的原因是控制流大致如下工作:

    1. 测试条件
    2. 如果为假,则终止
    3. 如果为真,执行正文
    4. 执行递增步骤

    因为 (1) 和 (4) 是解耦的,所以可以使用前自增或后自增。

    【讨论】:

    • 鉴于i++在递增后需要记住i的旧值,我认为++i可能会更短(大约1-2条指令)。
    • 虽然未使用的值应该被优化掉,对吧?
    • @JaminGrey 除非你有使用 i++ 的理由,否则养成 ++i 的习惯会有什么伤害?
    • Azendale 做对了。默认使用 postinc/decrement 是很多人的一个非常不幸的习惯。只有在有明确且可辩护的理由时才应使用帖子。当然,在任何重要的编译器中,未评估的原语可能不会有区别,但这并不意味着 post 应该是默认选择;这样的习惯最终只是自找麻烦。
    • 如果 C++ 被称为 ++C,很可能默认习惯是 ++i :)
    【解决方案3】:

    因为在任何一种情况下,增量都是在循环体之后完成的,因此不会影响循环的任何计算。如果编译器很愚蠢,则使用后增量可能会稍微降低效率(因为通常它需要保留 pre 值的副本以供以后使用),但我希望有任何差异在这种情况下进行了优化。

    考虑一下 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
    

    【讨论】:

      【解决方案4】:

      i++和++i都是在每次执行完printf("%d", i)之后执行的,所以没有区别。

      【讨论】:

        【解决方案5】:

        嗯,这很简单。上面的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 首先递增,或预递增,因此得名)和第二个代码块 @987654331 @ 在增量之前看到i 的值。

        【讨论】:

          【解决方案6】:

          编译器翻译

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

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

          所以在你的情况下(后/前增量)没关系。

          编辑:continue 被简单地替换为goto end;

          【讨论】:

          • 其实这样不太对。以for(int i = 0; i &lt; 42; i++) { printf("%d", i); continue; } 为例。您的主张在语义上等同于 int i = 0; while(i &lt; 42) { printf("%d", i); continue; i++; },这显然是错误的。
          • 查看我的编辑。该声明不是来自我,而是来自我读过的一本关于编译器的书,如果我记得很清楚的话是这本书amazon.com/Compilers-Principles-Techniques-Alfred-Aho/dp/…
          • 我怀疑龙书里有这样的错误。无论哪种方式,也许需要更仔细地阅读?
          【解决方案7】:

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

          解决方案:

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

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

          可以写成:

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

          您可以看到操作总是在循环的底部完成。在这种形式中,i++++i 将具有相同的效果应该很清楚:它们都会增加 i 并忽略结果。 i 的新值直到下一次迭代开始时才被测试,在循环的顶部。


          编辑:感谢 Jason 指出如果循环包含控制语句(例如 continue ) 这将阻止OPERATIONwhile() 循环中执行。 OPERATION总是for() 循环的下一次迭代之前执行。


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

          首先,如果候选人立即说出正确答案,只需一两分钟,因此我们可以直接进入下一个问题。

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

          在这种情况下,我要求他们使用while() 重写循环,这确实让我对他们的思维过程有了一个很好的了解。这就是我首先提出这个问题的原因:我想知道他们是如何解决问题的,以及当我对他们的世界运作方式产生怀疑时他们如何处理。

          此时,大多数考生都意识到了自己的错误并找到了正确答案。但我有一个坚持他原来的答案是正确的,然后改变了他将for()翻译成while()的方式。这是一次精彩的采访,但我们没有提供报价!

          希望有帮助!

          【讨论】:

          • 好吧,你应该重新考虑问这个面试问题,因为你犯了一个严重但常见的错误。 for 循环不能像您通常指定的那样重写。以for(int i = 0; i &lt; 42; i++) { printf("%d", i); continue; } 为例。您声称它在语义上等同于 int i = 0; while(i &lt; 42) { printf("%d", i); continue; i++; },这显然是错误的。
          • @Jason:今天我学到了我没有考虑过的边缘情况!近十年来,我从未有候选人认识到这一点。如果我继续使用这个问题,由于我提到的原因,这很有价值,我一定会根据上面的编辑重新表述它。如果候选人立即提供正确答案,我会询问是否有任何例外。 :-) +1 为您提供明确的反例。谢谢!
          • @Jason,我是否遗漏了什么,或者您对问题的回答是否相同。 @Adam Liss,我认为您的陈述是对的,请参阅我的回答的补充。您确实也可以很容易地实现 continue。
          • @jdehaan:我没有说一般的for 循环等同于你和@Adam Liss 给出的一般的while 循环。我说在这个特定的例子中,OP 的 for 循环和我给出的 while 循环之间的语义是相同的。一般来说,从for 循环到while 循环的天真转换是有问题的,这就是你遇到麻烦的原因。作为一个切线注释,请注意编译器甚至不需要将 OP 的 for 循环转换为任何类型的循环;例如,编译器可以完全展开循环。
          • @Jason:是的,我通常从看似简单的面试问题开始,然后根据应聘者的回答“挖掘”一下。到目前为止,我的大部分经验都是在一家小型工程公司工作,该公司倾向于避开新毕业生,并且偏爱 EE,而不是更专注于 CS 的开发人员。 (不是我的判断,只是它的方式。)无论如何,我很高兴我的回答引发了这次讨论,并感谢您的想法。
          【解决方案8】:

          如果出现以下情况,则会有所不同:

          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;
          }
          

          结果:

          inside post incement = 0

          i = 循环 0 中的后增量

          inside post incement = 1

          i = 循环 1 中的后增量

          第二个for循环:

          inside preincement = 0

          i = 循环 1 中的预增量

          inside preincement = 1

          i = 循环 2 中的预增量

          【讨论】:

          • 这个区别与for循环无关;这是因为您在语句中有副作用时使用了前/后增量。
          【解决方案9】:

          for 构造中的第三条语句只被执行,但它的评估值被丢弃并且不被处理。
          当评估值被丢弃时,前后增量相等。
          它们只有在取值时才会有所不同。

          【讨论】:

            【解决方案10】:

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

            但是,在底层有一个区别:后增量i++需要创建一个临时变量来存储i的原始值,然后执行递增并返回临时变量。预增量++i 不会创建临时变量。当然,当对象像int 这样简单时,任何体面的优化设置都应该能够将其优化掉,但请记住,++ 运算符在更复杂的类(如迭代器)中被重载。由于这两个重载方法可能有不同的操作(例如,一个可能想要输出“嘿,我是预递增的!”到标准输出)编译器无法判断当返回值不使用时这些方法是否等效(基本上是因为这样的编译器会解决无法解决的halting problem),如果你写myiterator++,它需要使用更昂贵的后增量版本。

            你应该预增的三个原因:

            1. 您不必考虑变量/对象是否可能具有重载的后自增方法(例如在模板函数中)并区别对待(或忘记区别对待)。
            2. 一致的代码看起来更好。
            3. 当有人问您“为什么要预先增加?”您将有机会向他们介绍停止问题和theoretical limits of compiler optimization。 :)

            【讨论】:

            • 我觉得你错了! ++i 和 i++ 之间的唯一区别是你做事的顺序。在一种情况下你会 inc *i push *i 而在另一种情况下你会 push *i inc *i 我不会认为这是一种优化。
            • 他没有错。当您不使用副本时,后递增原语可以很容易地被编译器优化出来。但他在最后一点提到的一点是,迭代器的后递增是作为方法调用和构造一个全新的迭代器副本来实现的。这些不能被编译器优化出来,因为构造函数和函数调用可能会产生编译器无法跟踪的副作用,因此不能假设它们不是关键的。
            • > 在一种情况下你会 inc *i push *i 而在另一种情况下你会 push *i inc *i 我不认为这是一种优化。
            • 他错了:我在这里尝试了编译器资源管理器的前后增量:godbolt.org,无论您以哪种方式添加增量,您都会得到完全相同的程序集。使用 C,您只是在描述您希望编译器发生的事情,如果它等同于相同的事情,那么您通常会得到相同的输出,这些天编译器非常聪明。前后增量几乎没有任何区别 - 如果您不相信我,请尝试编译器资源管理器!
            • 当然,但是你是用基本类型还是用一些更复杂的类型来检查它?
            【解决方案11】:

            如果你这样写,那就很重要了:

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

            如果这样写,会重复一次:

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

            【讨论】:

              【解决方案12】:

              您可以在此处阅读 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;
              }
              

              【讨论】:

              • 这并不能回答为什么输出相同的问题。
              • @Bo Persson,公平地说,您不仅应该为我的回答打分。我觉得你真的很粗鲁。
              • 也许我是,但其他答案是 2 岁,实际上尝试回答 “我想知道为什么没有区别”
              • @Bo Persson,刚刚找到并回答,没有提到日期。
              猜你喜欢
              • 2016-02-13
              • 2013-08-26
              • 2016-05-02
              • 2014-08-11
              • 2013-06-26
              • 1970-01-01
              • 2014-12-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多