在for循环中使用++i或i++?

63
9个回答

136

++i的语义略微更高效:

++i;  // Fetch i, increment it, and return it
i++;  // Fetch i, copy it, increment i, return copy

对于类似于整数的索引,效率提升最小(如果有的话)。对于迭代器和其他较重的对象,避免复制可以是一个真正的优势(特别是如果循环体内没有太多的工作)。

例如,考虑使用提供任意精度整数(因此具有类似于向量的内部结构)的理论BigInteger类的以下循环:

std::vector<BigInteger> vec;
for (BigInteger i = 0; i < 99999999L; i++) {
  vec.push_back(i);
}

在这个循环中,i++ 操作包括了拷贝构造(即 operator new, 逐位数字拷贝)和析构(operator delete)。而这个循环本质上只会多做一次索引对象的拷贝。所以,使用后缀增量运算符 i++ 而不是前缀增量运算符 ++i,实际上会让程序多做一倍的工作(并且很可能会增加内存碎片化)。


1
伟大而简洁的例子 - s g
4
我想给你的答案点赞,但这样它就不会再被评为42分了。 - Mouradif
1
@KiJéy,你现在可以回来做了。但是这样它就不会再被评为64了... - wizzwizz4
1
@wizzwizz4,让我们把它调整到128。 - Mouradif

92

对于整数而言,前置和后置自增没有区别。

如果i是一个非平凡类的对象,则通常更喜欢使用++i,因为对象会先被修改然后再进行计算,而i++则是在计算之后再进行修改,因此需要进行复制。


18
尽管使用整数生成相同的代码,但由于第二段的原因,建议对所有类型都使用 ++i,以便您养成使用它的习惯。 - SingleNegationElimination
2
@Token: 我基本上同意。然而,在C代码中,i++有点成为惯用语;如果你经常使用这两种语言,可能会感到困惑... - Oliver Charlesworth
请原谅我的无知,但这听起来确实很傻。假设i是某个类的实例,该类重载了递增运算符。您是说,如果我只是后置递增(不在循环控制语句中,而是仅递增它)(即i ++;),编译器将无法看到我没有使用返回值,并自动重新编写为++ i - Jim Mischel
4
@Jim:编译器必须调用相关的运算符重载(分别是前自增operator++()和后自增operator++(int))。如果它们实际上是等价的,并且如果调用是内联的,并且涉及到的复制都被优化器移除,那么生成的代码可能会相同-可以尝试一下。但是编译器肯定不能仅基于结果未使用就将其中一个替换为另一个。我并不特别在意,因为我写++i以明确我的意图。“increment i” -> “++i”。 - Steve Jessop
2
@Jim Mischel:编译器可以看到返回值没有被使用,但它没有理由期望 ++i 和 i++ 在复制模数方面具有相同的语义。因此它必须调用程序员要求的版本。 - Drew Hall
显示剩余3条评论

10

++i 是前缀递增,i++ 是后缀递增。
后缀递增的缺点是它会生成一个额外的值;它返回旧值的副本,同时修改i。因此,在可能的情况下应避免使用后缀递增。


5

对于整数,使用哪个都可以。

如果循环变量是一个类/对象,这可能会有所不同(只有通过性能分析才能告诉您是否存在显着的差异),因为后缀递增版本要求您创建一个被丢弃的该对象的副本。

如果创建该副本是一个昂贵的操作,那么您每次通过循环都必须支付一次这样的费用,而没有任何理由。

如果您习惯于在for循环中始终使用++i,那么您不需要停下来思考在特定情况下您正在做什么。您总是这样做。


3

这是由于性能原因:i++ 生成了一个副本,如果您立即丢弃它,这是一种浪费。当然,如果 i 是原始类型,编译器可以优化掉这个副本,但如果不是,则不能。请参见问题。


1
这是不真诚的。在几乎每一个情况下,++ii++的性能完全相同。 - John Dibling
3
@John:一点也不虚伪。对于迭代器对象,编译器甚至没有理由期望前缀和后缀增量之间存在某种等价关系(类设计者可以使它们执行任意不同的操作)——必须调用后缀形式(并产生副本)。对于重型迭代器和其他轻量级循环,性能损失可能会显著。举个例子,如果 i 不是 int 或 std::vector<int>::iterator,而是 BigInteger 或等效类型,则复制是不必要的,应该避免使用。 - Drew Hall
1
@Drew:在这种情况下,程序员会知道它(或者应该知道),我的免责声明“如果你必须问,那就不重要”不适用。问题是关于“普通for循环”的。性能与此无关。这完全取决于个人风格。 - John Dibling
1
John Dibling说:“普通的for循环”通常涉及迭代器。 - M2tM
1
@John:也许吧。我想我只是认为这是一个好习惯--总是写++i没有任何坏处,但有时写i++会有坏处。 - Drew Hall
@Drew:我不否认这是一个好习惯。事实上,我也是这样做的。 - John Dibling

3

任何值得一用的编译器在运行时都不会因为操作系统或硬件平台而有所不同。

for(int i=0; i<10; i++)

and

for(int i=0;i<10;++i)

++i和i++的成本是相同的。唯一不同的是,++i的返回值为i+1,而i++的返回值为i。

因此,对于那些更喜欢使用++i的人来说,可能没有合理的理由,这只是个人喜好。

编辑:对于类来说,这是错误的,如其他每篇帖子所说。如果i是一个类,i++会生成一个副本。


1
这可能对于类似int的索引是正确的,但对于迭代器和其他可能充当索引的对象来说并不总是正确的。 - Drew Hall
i++和++i调用的不是同一个运算符吗? - rtpg
1
不,++i 调用 i.operator++(),而 i++ 调用 i.operator(int)。有关实现细节,请参见我的答案 - fredoverflow
完全同意@FredOverflow所说的。可以这样想,没有理由写i++,但可能有理由写++i。 - Ajk_P

2

正如其他人已经指出的那样,对于用户定义类型,前置递增通常比后置递增更快。为了理解这是为什么,看一下实现两个运算符的典型代码模式:

Foo& operator++()
{
    some_member.increase();
    return *this;
}

Foo operator++(int dummy_parameter_indicating_postfix)
{
    Foo copy(*this);
    ++(*this);
    return copy;
}

如您所见,前缀版本只是修改对象并通过引用返回它。

另一方面,后缀版本必须在实际增量执行之前进行复制,然后将该副本按值复制回调用者。从源代码可以明显看出后缀版本必须做更多的工作,因为它包括对前缀版本的调用:++(*this);

对于内置类型,只要您丢弃该值,即只要不在较大的表达式中嵌入++ii++,例如a = ++ib = i++,就没有任何区别。


2
我认为“必须复制”有点夸张了。如果结果未使用,并且运算符和复制构造函数都可以完全内联,就像迭代器通常的情况一样,那么它应该是一个很容易被优化器移除copy的靶子——未使用的值没有副作用。++i确实可以避免你想知道优化是否成功,更重要的是它显然更有意义;-) - Steve Jessop

1

当您使用后缀运算符时,它会在内存中实例化一个对象。有些人认为在for循环中使用后缀运算符更好。


-1

个人喜好。

通常来说,有时候这很重要,但是不想显得像个自大的人,如果你必须问的话,那可能并不重要。


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