为什么说++i比i++效率高?

不知道你是否听说过++i比i++快的说法,真的如此吗?

++i与i++的区别

  • 这两个表达式从我们初学编程语言的时候就会接触到。前者是自增后取值,后者是取值后自增。

  • 我们看一个简单的例子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    using namespace std;
    int main()
    {
    int a = 0;
    int b = 0;
    int c = a++;//int tmp = a;c=a;a = a + 1
    int d = ++b;//b = b + 1;d = b;
    cout<<"c="<<c<<";d="<<d<<endl;
    return 0;
    }
  • 运行结果:

    1
    c=0;d=1
  • 对于这个结果我们并不感到意外。

  • 另外我们还注意到另外一个有意思的现象:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;
int main()
{
int a = 0;
int b = 0;
int *c = &(a++);
int *d = &(++b);
return 0;
}
  • 编译后报错:

    1
    2
    main.cpp:7:19: error: lvalue required as unary ‘&’ operand
    int *c = &(a++);
  • 说&作用于左值,也就是说a++的结果并非左值。但++b的结果是。

  • 可简单理解左值和右值:

    1. 左值,有名对象,可赋值
    2. 右值,临时对象,不可被赋值

运算符重载

  • 在《运算符重载》一文中已经说到了运算符的重载,通过前面的例子也发现了,对于内置类型,前置自增返回对象的引用,而后置自增返回对象的原值(但非左值)。

  • 基于上述原则,一个前置版本和后置版本的常见实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test
{
public:
Test& operator++();//前置自增
const Test operator++(int);//后置自增
private:
int curPos; //当前位置
};
/*前置自增实现范式*/
Test& Test::operator++()
{
++curPos; //自增
return *this; //取值
}
/*后置自增实现范式,为了与前置区分开,多了一个int参数,但从来没用过*/
const Test Test::operator++(int)
{
Test tmp = *this; //取值
++curPos; //自增
return tmp;
}
  • 仔细观察后,我们发现前置自增,先自增,后返回原对象的对象;没有产生任何临时对象;而后置自增,先保存原对象,然后自增,最后返回该原临时对象,那么它就需要创建和销毁,这样一来,效率孰高孰低就很清楚了。

  • 在不进行赋值的情况下,内置类型前置和后置自增的汇编都是一样的呢!

1
2
3
4
5
6
void test()
{
int i = 0;
i++;
//++i;
}
  • 汇编:

    1
    2
    3
    4
    5
    6
    7
    push    rbp
    mov rbp, rsp
    mov DWORD PTR [rbp-4], 0
    add DWORD PTR [rbp-4], 1
    nop
    pop rbp
    ret
  • 不过,赋值的情况下,并且不开启编译器优化,它们的汇编代码还是有差别的,有兴趣的可以试试。

总结

  • 对于内置类型,前置和后置自增或者自减在编译器优化的情况下,两者并无多大差别,而对于自定义类型,如无特别需要,人们似乎更加偏爱前置自增或自减,因为后置自增常常会产生临时对象。