其他人已经解决了后置自增和前置自增的函数差异。
就左值而言,i++
不能被赋值,因为它不是指向变量,而是指向计算出的值。
在赋值方面,以下两者都同样没有意义:
i++ = 5;
i + 0 = 5;
由于前缀递增返回的是递增后变量的引用而不是临时副本,因此++i
是一个左值。
出于性能原因而更喜欢使用前缀递增,在递增诸如迭代器对象(例如在STL中)之类的重量级对象时尤其明智。
正如另一个回答者已经指出的那样,++i
之所以是左值,是为了将其传递给引用。
int v = 0;
int const & rcv = ++v; // would work if ++v is an rvalue too
int & rv = ++v; // would not work if ++v is an rvalue
void taking_refc(int const& v);
taking_refc(10); // valid, 10 is an rvalue though!
Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)
int const & rcv = ++v; // would work if ++v is an rvalue too
是可以工作的,但是它会以不同的方式工作。特别地,int const & rcv = v++;
不会将一个引用绑定到 v
上,未来对 v
的更改在读取 rcv
时将不可见。 - Ben Voigt++i
是lvalue,但是没有解释为什么,也就是说,为什么C++标准委员会加入了这个特性,特别是考虑到C语言都不允许这样做。根据comp.std.c++上的这次讨论,这样做可以让你取它的地址或者赋值给一个引用。以下是Christian Bau帖子中提取的代码示例:
顺便说一下,如果int i; extern void f (int* p); extern void g (int& p);
f (&++i); /* 在C语言中是非法的,但C程序员并不需要此功能 */ g (++i); /* C++程序员希望这是合法的 */ g (i++); /* 在C++中不合法,而且给这个具有实际意义将会很困难 */
i
是内置类型,那么类似++i = 10
这样的赋值语句会导致未定义行为,因为i
在序列点之间被修改了两次。i++ = 2;
但我把它改成时就不行了
++i = 2;
原问题的答案:
如果你在一个语句中单独使用增量运算符(比如在 for 循环中),那么两者并没有太大区别。前缀自增似乎更高效,因为后缀自增需要对自身进行增量并返回临时值,但编译器会优化这种差异。
for(int i=0; i<limit; i++)
...
等同于
for(int i=0; i<limit; ++i)
...
当您将操作的返回值用作较大语句的一部分时,情况会变得有些复杂。
即使是这两个简单的语句
int i = 0;
int a = i++;
并且
int i = 0;
int a = ++i;
这两种不同的增量运算符是不同的。在多个操作符语句中使用哪种增量运算符取决于预期的行为。简而言之,不能只选择其中一种。你必须了解两种。
前置自增应该在表达式之前对对象进行自增,并且可以在此表达式中使用,就好像已经发生了这种情况一样。因此,C++标准委员会决定它也可以用作左值。
后置自增应该增加POD对象并返回一个副本以用于表达式(请参见n2521第5.2.6节)。由于副本实际上不是变量,因此将其作为左值没有任何意义。
对象的前置和后置自增只是语言提供的一种调用对象方法的手段。因此,从技术上讲,对象不受语言标准行为的限制,而仅受方法调用所施加的限制。
实现这些方法的人需要使这些对象的行为反映出POD对象的行为(这不是必需的,但是预期这样做)。
这里的要求(预期行为)是增加对象(意味着依赖于对象),并且该方法返回一个可修改的值,看起来像在增加发生之前的原始对象(就像在此语句之前发生了增量一样)。
要做到这一点很简单,只需要使该方法返回对自身的引用。引用是一个左值,因此将按预期工作。
这里的要求(预期行为)是对象增量(与前置自增相同),并且返回的值看起来像旧值,并且是不可变的(以便它不像左值一样行为)。
不可变:
要做到这一点,应该返回一个对象。如果在表达式中使用该对象,则将其复制构造到临时变量中。临时变量是const的,因此它将是不可变的,并且将按预期工作。
看起来像旧值:
这可以通过在进行任何修改之前创建原始副本(可能使用复制构造函数)来简单实现。复制品应该是深度复制,否则对原始的任何更改都将影响复制品,因此状态将相对于使用对象的表达式而言发生变化。
与前置自增相同:
最好基于前置自增来实现后置自增,以便获得相同的行为。
class Node // Simple Example
{
/*
* Pre-Increment:
* To make the result non-mutable return an object
*/
Node operator++(int)
{
Node result(*this); // Make a copy
operator++(); // Define Post increment in terms of Pre-Increment
return result; // return the copy (which looks like the original)
}
/*
* Post-Increment:
* To make the result an l-value return a reference to this object
*/
Node& operator++()
{
/*
* Update the state appropriatetly */
return *this;
}
};
++i
在某个表达式中可用听起来像是一段不好的代码,不是吗?它怎么可能成为C ++标准委员会的原因呢? - KindreddoStuff(++i);
你想要先将 i
增加,然后将其作为参数传递给 doStuff()
。相反,doStuff(i++)
增加了 i
的值,但是传递给 doStuff()
的是 i
的原始值(增加之前的值)。 - Martin York++i
或i++
,然后再调用我在上面评论中提到的doStuff(i)
呢?因为我引用的部分就像是在说“我们确保++i
先完成,所以现在你可以调用doStuff(++i)
这样的东西”,而在我看来,这是一段糟糕的代码。所以我在考虑不同的原因。 - Kindredi = ++i
这样的表达式就是可取的,因为++i
现在已经在表达式之前被增加了。我知道这个问题本身有点矛盾,因为在我看来,++i
只是一个简写,使得任何事情都变得方便(但不一定易于理解),但我想委员会可能还有其他好的理由。抱歉让您读了这么长的内容。 - KindredRegarding LValue
In C
(and Perl for instance), neither ++i
nor i++
are LValues.
In C++
, i++
is not and LValue but ++i
is.
++i
is equivalent to i += 1
, which is equivalent to i = i + 1
.
The result is that we're still dealing with the same object i
.
It can be viewed as:
int i = 0;
++i = 3;
// is understood as
i = i + 1; // i now equals 1
i = 3;
i++
on the other hand could be viewed as:
First we use the value of i
, then increment the object i
.
int i = 0;
i++ = 3;
// would be understood as
0 = 3 // Wrong!
i = i + 1;
(++i) = 5;
,那么仍然是未定义的。括号会强制先进行递增运算吗? - v010dyai = <value>
呢? - Martin York主要的区别在于i++返回前增量值,而++i返回后增量值。我通常使用++i,除非我有一个非常强烈的理由使用i++ - 也就是说,如果我真的需要前增量值。
在我看来,使用'++i'形式是一个好的实践。虽然在比较整数或其他POD时,前增量和后增量之间的差异并不真正可测量,但当使用'i++'时,你必须进行额外的对象复制并返回,如果对象要么很昂贵,要么经常增加,这可能会对性能产生重大影响。
i++
和 ++i
视为相同。 - Kindred也许这与后置递增的实现方式有关。可能是像这样的:
由于该副本既不是变量,也不是动态分配内存的引用,因此它不能是左值。
编译器如何翻译这个表达式?a++
我们知道我们想要返回未递增的版本a
,即在递增之前的旧版本a
。我们还希望通过副作用递增a
。换句话说,我们返回的是a
的旧版本,它不再代表a
的当前状态,它不再是变量本身。
返回的值是一个 a
的副本,它被放置在一个寄存器中。然后变量被递增。所以在这里你不是返回变量本身,而是返回一个分离的实体!这个副本临时存储在一个寄存器中,然后被返回。回想一下,在C++中,一个左值(lvalue)是一个在内存中有可识别位置的对象。但是这个副本存储在CPU的一个寄存器中,而不是内存中。所有的右值(rvalue)都是没有可识别位置的对象。这就解释了为什么 a
的旧版本的副本是一个右值,因为它被临时存储在一个寄存器中。通常情况下,任何副本、临时值或者类似 (5 + a) * b
这样的复杂表达式的结果都会被存储在寄存器中,然后被赋给变量,它是一个左值。
后缀操作符必须将原始值存储在寄存器中,以便可以将未递增的值作为其结果返回。 考虑以下代码:
for (int i = 0; i != 5; i++) {...}
这个 for 循环计数到五,但是 i++
是最有趣的部分。它实际上是两条指令合并成一条。首先,我们必须将 i
的旧值移动到寄存器中,然后再将 i
递增。在伪汇编代码中:
mov i, eax
inc i
eax
寄存器现在包含了变量 i
的旧版本的副本。如果变量 i
存储在主内存中,CPU 可能需要花费很长时间去获取这个副本并将其移动到寄存器中。对于现代计算机系统来说,这通常非常快速,但如果您的 for 循环迭代了十万次,所有这些额外操作开始累积起来!这将导致显著的性能损失。
现代编译器通常足够聪明,可以优化整数和指针类型的这些额外工作。对于更复杂的迭代器类型或类类型,这些额外工作可能会更加昂贵。
那么前缀递增 ++a
呢?
我们想要返回已经递增的版本 a
,即递增后的新版本 a
。新版本的 a
表示 a
的当前状态,因为它就是变量本身。
首先,a
被增加了。既然我们想要得到更新后的 a
,为什么不直接返回变量 a
本身 呢?我们不需要将其复制到寄存器中生成 rvalue 的临时副本。那样会多做无用功。因此,我们只需将变量本身作为 lvalue 返回。
如果我们不需要未增加的值,就没有必要将旧版本的 a
复制到寄存器中,这是由后缀运算符完成的。这就是为什么只有在真正需要返回未增加的值时才应该使用 a++
。对于其他所有目的,只需使用 ++a
。通过习惯性地使用前缀版本,我们就不必担心性能差异是否重要。
使用 ++a
的另一个优点是它更直接地表达了程序的意图:我只想增加 a
!然而,当我在别人的代码中看到 a++
时,我会想知道他们为什么要返回旧值?这是为什么呢?