++i 和 i++
在不涉及到类的前加和后加,除了代码顺序的差异,其实在效率上来说并没有什么区别。如果加上编译器的优化,他们生成的汇编指令可能是完全相同的。但是我们今天主要就讨论这样的情况。
如果把i++比做一个函数,由于语法规则的要求,它只能把加1千的原始值,返回给主调函数。所以,在自加1之前,它需要用一个临时变量,保存自己的原始值,而构建原始原谅的这个操作就是占用内存和CPU资源的始作俑者。
如果我们的临时变量是一个类,这个构建临时对象的开销无法忽略:
重载前置 ++ 运算符
首先明确前置 ++ 运算符的
语义:它首先对对象执行自增操作,然后返回自增后的对象自身。
具体实现:由于前置 ++ 运算符直接返回自增后的对象自身,返回类型应为对象的引用。这样可以避免不必要的对象拷贝,提高效率。
class A {
public:
int x;
A& operator++() {
x++;
return *this;
}
}
重载后置++运算符
语义:后置 ++ 运算符首先保存当前对象的状态,然后对对象执行自增操作,最后返回保存的未自增的对象状态。
具体实现:明确语义后,实现就很直观了,由于后置 ++ 运算符需要返回自增前的对象状态,因此返回类型应为对象的副本。这意味着需要创建一个对象的临时副本,并在返回前进行对象状态的保存。
我们在刚才类的基础上实现:
A operator++(int) {
A tmp = *this;
++*this;
return tmp;
}
在运算符重载中,参数列表是运算符函数的一个重要组成部分。为了区分前置和后置 ++ 运算符,后置 ++ 运算符需要一个额外的 int 参数。这个参数并不实际使用,仅仅作为一个标识。
重载后置++运算符的汇编
大头来了,我们一起看看他的汇编代码:
首先是构建临时对象的汇编A tmp = *this;
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-4], eax
嗯嗯构建临时对象感觉还好,接下来是++*this;
mov rax, QWORD PTR [rbp-24]
mov rad, rax
call A::operator++()
!!!他竟然调用了前加函数!
这样的话后加(i++)需要包含一个完整的前加(++i)操作,这种差异性能当然天差地别!
总结:我们总是在讨论++i和i++的性能问题,其实在很多时候,他们两个除了语法的差异外,在汇编的面前都是几条简单、直接的指令。
但是只要涉及到类,问题就完全不一样了,因为后加回包含一次完整的前加操作,如果这个类的数据及其庞大,这对效率的影响是非常大的。
热点面试题
- 前加(++i)是左值的,这是什么意思?
前加的返回值,是一个变量的引用,该引用是可以被赋值的。例如,我们可以把2赋给前加,但是不能把2赋给后加,因为后加是右值的,如下代码:
void func_3() { int i = 1; ++i = 2; //i++ = 2; //错误! }
- 如果i = 1,(++i)++应该等于多少
不建议写这样的代码,非常不方便阅读。因为它涉及到未定义行为(undefined behavior)。未定义行为意味着C++标准并未规定在这种情况下应该如何执行程序,因此不同的编译器可能会有不同的解释和处理方式。
其次,不同编译器对该代码的解释会有区别。如果只针对特定的编译器,例如GCC,他的结果是可以预期的;但是如果使用Clang和Visual Studio编译运行可能会得到不同的结果 。因为
- GCC:在某些情况下,GCC可能会按照一定的顺序进行评估,使得结果是可以预测的,但这种行为并不受C++标准保障。
- Clang和Visual Studio:这些编译器可能会采用不同的优化和评估策略,导致不同的结果,或者可能直接产生编译错误或运行时错误。