本文主要讨论的是运算符重载中有关++/–(增量和减量)的重载并介绍了前缀后缀应用机制的发展演变。

##运算符重载的基本概念

在c++语言中,可以用关键字operator加上运算符来表示函数,叫做运算符重载。运算符重载函数是一种特殊形式的函数,即运算符本身就是函数名,并且不改变它们作为内置运算符时的使用方法。例如两个复数相加函数:

Complex Add(const Complex& a, const Complex& b);

可以用运算符重载来定义:

Complex operator+(const Complex& a, const Complex& b);

运算符与普通函数在调用时的不同之处在于:对于普通函数,实参出现在圆括号内;而对于运算符,实参出现在其两侧(或一侧)。运算符重载是支持数据抽象和泛型编程的利器。比如一般来说,凡是用作容器元素类型的class(包括struct)都需要重载“=”、“==”和“<”等运算符,因为容器可能会使用它们来排序或者拷贝元素,某些泛型算法都是以假设元素类型已经重载了“=”和“<”为前提的。

##重载++和–

重载++和–这两个运算符之所以令许多人困惑,是因为它们都存在前置版本和后置版本。许多教科书中关于后置版本的语义解释并不完全正确,而在c++程序中,你可能要为某些类重载“++”和“–”运算符,正确理解它们的语义是非常重要的。c++标准规定:当为一个类型重载“++”/“–”的前置版本时,不需要参数;当为一个类型重载“++”/“–”的后置版本时,需要一个int类型的参数作为标志(哑元,非具名参数)。想知道这个哑元的由来么?为什么会是这样,而不是通过一个特殊的关键字来标识这种位置关系,《the design and evolution of c++》中是这样说的:

增量运算符++和减量运算符–都是用户可以定义的运算符,但是release1.0并没有提供区分前缀或后缀应用的机制,于是乎:

class Ptr{
  // ...
  void operator++();
};

在这两种情况中使用的都是同一个Ptr:operator++():

void f(Ptr& p){
  p++;    // p.operator++()
  ++p;    // p.operator++()
}

Bjarne收到很多的建议,特别是Brian Kernighan指出,从c语言的观点来看,这种限制是不自然的,它也阻止了人们定义那种能够用来取代常规指针的类。他考虑到了最明显的解决办法,在c++里增加关键字prefix和postfix:

class Ptr_to_X{
  // ...
  X& operator prefix++();    //prefix ++
  X operator postfix++();    //postfix ++
};

或者:

class Ptr_to_X{
  // ...
  X& prefix operator++();    //prefix ++
  X postfix operator++();    //postfix ++
};

这样一来,又有一些人开始叫喊了:“随随便便”增加关键字什么的最讨厌了!人们建议了几个并不涉及新关键字的方案。例如:

class Ptr_to_X{
  // ...
  X& ++operator();    //prefix ++
  X operator++();      //postfix ++
};

或者:

class Ptr_to_X{
  // ...
  X& operator++(); //prefix because it returns a reference
  X operator++();   //postfix because it doesn't return a reference
};

c++之父觉得前一个太做作,而后一个又太微妙(我既没有看出哪个做作也看不出什么微妙,惭愧),最后停止在了这个可能是既做作又微妙的方案上(不一般的思维):

class Ptr_to_X{
  // ...
  X& operator++();    //prefix: no argument
  X operator++(int);  //postfix: because of the argument
};

c++设计与演化关于此的原话摘录如下:

This may be both too cute and too subtle, but it works, requires no new syntax, and has a logic to the madness. Other unary operators are prefix and take no arguments when defined as member functions. The “odd” and unused dummy int argument is used to indicate the odd postfix operators. In other words, in the prefix case, ++ comes between the first(real) operand and the second(dummy) argument and is thus postfix.

These explanations are needed because the mechanism is unique and therefore a bit of a wart. Given a choice, I would probably have introduced the prefix and postfix keywords, but that didn’t appear feasible at the time. However, the only really important point is that the mechanism works and can be understood and used by the few programmers who really need it.

最后举一个例子来说明如何重载这两个运算符

class Integer{
	public:
		Integer(long data):m_data(data){}
		Integer& operator++(){        //前置版本,返回引用
			cout<<"Integer::operator++() called!"<<endl;
			m_data++;
			return *this;
		}

		Integer operator++(int){       //后置版本,返回对象的值
			cout<<"Integer::operator++(int) called!"<<endl;
			Integer temp = *this;
			m_data++;                       
			return temp;                     //返回this对象的旧值
		}

		...
	private:
			long m_data;
};

int main(){
	Integer x = 1;    //call Integer(long), ?Integer(1)
	++x;              //call operator++()
	x++;              //call operator++(int)

	return 0;
}

当“++”/“–”应用于基本数据类型时,前置版本和后置版本在效率上没有多大差别。然而,当应用于用户定义类型,尤其是大对象的时候,前置版本就会比后置版本的效率高许多。后置版本总是要创建一个临时对象,在退出函数时还要销毁它,而且返回临时对象的值时还会调用其拷贝构造函数。所以,如果可以选择,请尽量使用前置版本。