自增、自减操作符前缀形式与后缀形式的区别。
基础
从你开始做 C 程序员那天开始,你就记住 increment 的前缀形式有时叫做“增加然后取
回”,后缀形式叫做“取回然后增加”。这两句话非常重要,因为它们是 increment 前缀与后
缀的形式上的规范。
- 能够理解上述“增加然后取回”和“取回后增加”就表示你能够简单运用这类运算符了,比如
int value_1 = 0; int temp_1 = value_1++; //后自增 cout<<value_1<<temp_1; //value_1 == 1,temp_1 == 0; int value_2 = 0; int temp_2 = ++value_2; //前自增 cout<<value_2<<temp_2; //value_2 == 1,temp_2 == 1;
- 从表面上看,无论是value_1还是value_2都增加了,但是它们各自赋值的对象的值却不相同。
- 上述应该很好理解,这要明白那两句话就没问题。
- 可能大家都知道的是前自增(++i)的效率比后自增(i++)的效率要高一些,但是大家知道为什么前自增效率要好一些吗?
- 可能大家都看到过一些这样的题目:
我相信只凭借上述两句话是不足以回答这些问题的全部的,下面我们将来了解一下这些运算符的实现原理并给出这些问题的答案和解释。int temp; int val = 0; temp = ++val; //temp=? temp = val++; //temp=? temp = ++++val; //temp=? temp = val++++; //temp=? val++ += val++; //val=? ++val += ++val; //val=? ++val += val++; //val=? val++ = ++val; //val=?
进阶
- 为了解决上述出现的问题,我们以C++里面的运算符重载来探究解决此类问题
- 首先给出进行测验的代码:
class Demo { private: int value_; public: Demo() :value_(0) { cout << "构造" << endl; } Demo(int value) :value_(value) { cout << "构造" << endl; } Demo(const Demo & demo) { cout << "构造" << endl; this->value_ = demo.value_; } Demo & operator++(){ cout << "前缀" << endl; value_ = value_ + 1; return *this; } const Demo operator++(int) { cout << "后缀" << endl; Demo temp = *this; ++(*this); return temp; } friend ostream & operator<<(ostream & os, const Demo & i); Demo & operator+=(const Demo & rhs) { cout << "加上" << endl; this->value_ += rhs.value_; return *this; } }; friend ostream & operator<<(ostream & os, const Demo & i); Demo & operator+=(const Demo & rhs) { this->value_ += rhs.value_; return *this; } }; ostream & operator<<(ostream & os, const Demo & value) { cout << value.value_; Demo::step = 0; return os; }
- 为了区分重载operator++运算符,不至于分不清前后自增,C++规定后缀形式有一个int类型参数。
- 从底层实现的代码上看,前缀形式效率确实比后缀形式更高。
Demo & operator++(){ //重载前自增 value_ = value_ + 1; return *this; (4) } const Demo operator++(int) { //重载后自增 Demo temp = *this; // (1) ++(*this); // (2) return temp; // (3) }
- (1)处有一次对象的复制构造,有效率开销
- (2)处会调用前缀的operator++
- (3)处还有一次对象的复制构造,有效率的开销
- (4)处只有一个引用对象的复制。
- 给出代码验证:
int main(void) { Demo i; i++; } /* 输出: 构造 // Demo i; 构造i对象 后缀 // 调用运算符重载函数 构造 // Demo temp = *this;对象的复制构造 前缀 // 调用前缀形式 构造 // return temp; 对象的复制构造 */
int main(void) { Demo i; ++i; } /* 输出: 构造 // Demo i; 构造i对象 前缀 // 调用运算符重载函数 */
- 从上述两段代码的演示结果来看,的确是++i效率更高,前缀形式比后缀形式要少两次对象复制构造的开销。这个开销可大可小,如果像我们验证代码这样的Demo类,Demo类大小仅为4字节(32bit),效率差距不会特别大。但如果重载对象很大的时候(比如包含虚函数),这时效率差距就会变得明显。
- 注意前缀的返回值是Demo&,后缀的返回值是const Demo
- 有了上述的讨论和底层代码展示,现在可以开始探究一开始的问题。
temp = ++++val
++++val
的调用方式为val.operator++().operator()
,调用连续operator++()
,由于前缀返回对象的引用,所以可以连续调用。- 调用一次
operator++()
val的值就加1,所以temp应该是为2。
temp = val++++
val++++
的调用方式为val.operator++(0).operator(0)
,调用连续的operator++(0),但是由于后缀返回对象为const 对象,连续调用失败。- 调用一次
operator++(0)
之后返回一个临时const 对象,const 对象无法调用非const方法,所以会编译出错。 - 如果想要能够连续调用
operator++(0)
,可以更改函数重载定义,但是这样不会得到正确的结果://返回值 去掉const限定 Demo operator++(int) { cout << "后缀" << endl; Demo temp = *this; ++(*this); return temp; }
- 再一次进行
temp = val++++
,不会编译出错,但是temp的值为0,val==1(val初值为0),为什么?- 第一次调用
operator++(0)
返回了一个临时非const对象 - 第二次调用
operator++(0)
只会修改第一次调用返回的临时对象的值,不是原始对象的值。 - 最后返回给temp的就是第二次调用operator++(0)返回的临时对象,值为0;
- val在第一次调用的时候增加了1,第二次调用与val无关。
- 所以修改为非const会出现违背人的直觉的情况(两次++却只加一次)
- 至于为什么后缀不返回引用,这里不再赘述。有意了解参见 Effective C++ 条款 21;
- 第一次调用
val++ += val++
(val = 1)- 运算符优先级
++
大于+=
,算式从右往左。先调用右边的val的后缀表达,再调用左边的val的后缀表达 - 右边val根据规则返回一个临时对象tval == 1
- 左边val经过右边的后缀调用,值变为val==2,然后左边val进行后缀调用,返回一个临时变量ttval == 2;
- 进行
+=
运算,但是左边ttval是一个const 临时对象,无法被赋值。 - 直接表现形式是编译错误
- 运算符优先级
++val += ++val
(val = 1)- 右边val进行前缀调用,根据规则返回一个val的对象引用,并且val == 2。
- 左边val进行前缀调用,根据规则返回一个val对象引用,并且val == 3。
- 执行
+=
,最后val=6。
++val += val++
(val = 1)- 右边val进行后缀调用,返回一个const 临时对象tval == 1,此时val == 2。
- 左边val进行前缀调用,返回一个val的对象引用,此时val == 3。
- 进行
+=
,val += tval,所以最后val值为4。
val++ = ++val
- 同上,编译错误
总结
- 这里只总结了
++
运算符,--
运算符和++
运算符思想是一样的,这里不再赘述。 - 前缀方式效率高于后缀方式,使用时尽量使用前缀方式。
- 前后缀返回值的不同,造成了不同表达式造成的不同结果,甚至不能通过编译,但是不用担心。真正实际开发并不会写诸如上述的代码,迷惑性太强。以上只是为了解析底层而刻意写出的代码。但是诸如面试题、考试题可能会出现这一类型的问题。
- 为了保证前后缀不会产生差异,给定一个实现原则:后缀的实现必须根据前缀形式来实现。