C++ 中的 i++ 和 ++i 是一对十分简洁但最容易让人忽视的操作,我已经对它们忽视了十多年,直到近日做一些迭代器时才有所体悟。在刚开始学习C++时虽然知道它们在加一操作上有一个先后的区别,但很难记住这个先后顺序,而且觉得只要不连带赋值操作时它们是等效的,所以也就没有花大力气去辨析它们,而是养成了单独使用它们的习惯,而且习惯性地只使用i++一个操作符。后来经常在各处论坛看到人们讨论这两个操作符的区别时,尤其是当有人拿出编译器生成的机器代码来对这两个操作符作辨析的较真劲时,觉得人们真的傻得有点可爱,也觉得在C++标准中保留这两个另人困惑的操作符的做法有点奇怪。用机器代码来辨析时,与编译器多少有点关联,本文尝试用操作符重载的方式来辨析这两个操作符,能够最大限度地降低对编译器的依赖。 ++i 与 i++ 的本质区别是:++i 操作除 i 之外不涉及新的(隐含的)操作数,而 i++ 则在 i 之外还涉及另一个新的(隐含的)操作数。 在开始辨析时,先提出三个习惯的改进建议: 1、优先使用 ++i: (a)单独使用时:++i; (b)作为循环控制变量使用时:for(int i=0; i!=100; ++i) ... 2、在局部范围内使用 ++i 时,若不出现逻辑错误,可尝试使用引用而不是赋值: int & j = ++ i; 3、杜绝两个以上的 i++ 或者 ++i 进行合成。 前两点建议对单纯的整数变量 i 所带来的好处可以忽略不计,但只要有可能使用非整数的场合,就有可能带来性能的改进。++i 和 i++ 是两个完全不同的操作符,如果要进行操作符重载的话,分别对应 ++() 和 ++(int),具体点即: ++i ===> _T& operator ++ () i++ ===> _T operator ++ (int) 下面定义一个简单的迭代器Iter<_T>来对这两个操作符进行辨析。迭代器通常被设计成一类可以在某些场合部分地模拟指针的行为进行特殊数据操作的工具。Iter<_T>的代码如下: template<typename _T> 说明: Iter<_T>::Iter(_T *) :初始化构造器接受一个指针作为初始化参数; Iter<_T>::Iter(const Iter &):复制构造器,里面输出一个 copy 字样用来指示调用了复制构造器; Iter & Iter<_T>::operator = (const Iter &) Iter & Iter<_T>::operator = (_T *) :两个赋值操作符,为了方便使用而引入; Iter & Iter<_T>::operator ++ ():对应 ++i 的操作符,注意返回值为 Iter &; Iter Iter<_T>::operator ++ (int):对应 i++ 的操作符,注意返回值为 Iter; Iter<_T>::operator _T * () const:类型转换操作符,使 Iter<_T> 具有代替 _T * 的能力。 使用Iter<_T>进行以下测试: int main() { int array [] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; Iter<int> it(array); cout << "*it: " << *it << endl; Iter<int>& it2 = ++ it; /*1*/ cout << "*it: " << *it << ", *it2: " << *it2 << endl; Iter<int> it3 = it ++; /*2*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << endl; Iter<int>& it4 = ++ it ++; /*3*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << endl; Iter<int> it5 = ++ it; /*4*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << endl; ++ it5; /*5*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << endl; Iter<int> it6 = ++ it ++; /*6*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << ", *it6: " << *it6 << endl; ++ it; /*7*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << ", *it6: " << *it6 << endl; it ++; /*8*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << ", *it6: " << *it6 << endl; ++ it ++; /*9*/ cout << "*it: " << *it << ", *it2: " << *it2 << ", *it3: " << *it3 << ", *it4: " << *it4 << ", *it5: " << *it5 << ", *it6: " << *it6 << endl; for(Iter<int> i=array; i!=array+10; ++i) /*10*/ cout << endl; for(Iter<int> i=array; i!=array+10; i++) /*11*/ cout << endl; return 0; } /*1*/之后输出的内容是:++() *it: 2, *it2: 2 /*2*/之后输出的内容是:++(int) copy() *it: 3, *it2: 3, *it3: 2 比较/*1*/和/*2*/容易看出 i++ 比 ++i 多调用了一个复制构造器。更仔细点分析容易知道,i++ 过程中先是分配了一个临时实例,然后把这个临时实例复制出来,最后还要把这个临时实例销毁掉。这就是 ++i 与 i++ 的本质区别,也就是为什么提倡优先使用 ++i 的根本原因。 /*3*/之后输出的内容是:++(int) copy() ++() *it: 4, *it2: 4, *it3: 2, *it4: 4 /*3*/的写法是不提倡的,因为这个过程开始引入了极难察觉的复杂性。从中可以看出是先执行了 i++,而后再复制,再后就是 ++i 。这个过程难以从表达式 ++ i ++ 中获得感性认识。按常人的理解,很容易解读为先执行了 ++ i,而后执行了 i++。另一个容易引入错误的是执行了这个表达式之后,it4 的内容开始变得模糊起来了,表面上看起来是对 it 的引用,但其实它具有嬗变性,也就是说它并不总是 it 的引用,这会在接下来的过程中看到它的变化。这也就是为什么要杜绝这类表达式的根本原因。 /*4*/之后输出的内容是:++() copy() *it: 5, *it2: 5, *it3: 2, *it4: 5, *it5: 5 /*4*/与/*1*/所不同的是在/*1*/处没有构造新的实例,it2是it的引用,而/*4*/则构造了新的实例,并调用了复制构造器。这里的另一个潜在的影响是对/*3*/的复合操作的,它潜在地改变了 it4 的引用,从此 it4 不再是 it 的引用,而是变成了 it5 的引用。 /*5*/之后输出的内容是:++() *it: 5, *it2: 5, *it3: 2, *it4: 6, *it5: 6 /*5*/的作用是对 it5 进行 ++i 操作,更清楚地确认 it4 的确变成了 it5 的引用,而 it2 仍然是 it 的引用,/*3*/中的复合表达式的不良影响在这里最终体现出来了。 /*6*/之后输出的内容是:++(int) copy() ++() copy() *it: 6, *it2: 6, *it3: 2, *it4: 6, *it5: 6, *it6: 6 /*6*/也是要杜绝的。但/*6*/与/*3*/相比更加安全些,因为在构造 it6 时使用了复制构造器,从此以后 it6 与 it 再无瓜葛。要杜绝的原因与/*3*/是相当的,因为即使比/*3*/安全,但它所复制的内容仍然具有一定程度上的嬗变性,难以相信这条语句结束后 it6 的内容与 it 的内容是一致的。 /*7*/之后输出的内容是:++() *it: 7, *it2: 7, *it3: 2, *it4: 6, *it5: 6, *it6: 6 /*8*/之后输出的内容是:++(int) copy() *it: 8, *it2: 8, *it3: 2, *it4: 6, *it5: 6, *it6: 6 /*7*/与/*8*/形成对比,类似于/*1*/和/*2*/。但值得注意的是在/*8*/中即使没有赋值操作,也需要构造一个临时变量,并释放这个临时变量。 /*9*/之后输出的内容是:++(int) copy() ++() *it: 9, *it2: 9, *it3: 2, *it4: 6, *it5: 6, *it6: 6 /*9*/虽然也是要杜绝的,但比/*6*/相比安全性更好一些,因为这里没有赋值操作。一般情况下都能保证 it 被加了两次。 /*10*/和/*11*/是在循环控制中使用 ++i 和 i++ 的对比,可以十分明显地看到 ++i 用在循环控制操作中的性能改进优势。 以上代码分析基于的编译器是 GCC 3.4.5。 轉自:http://hi.baidu.com/zighouse/blog/item/fd12fb0023907f1b7aec2ca6.html |