在C++中,有两种方法对运算符重载:重载为成员函数和重载为全局函数(非成员函数),显而易见的是,如果我们正在为一个不属于我们的类重载一个操作符,这个操作符应该是一个非成员函数。
运算符重载实质上就是函数重载,如果重载为成员函数,它就可以自由的访问本类的数据成员。实际使用时总是通过该类的某个对象来访问重载的运算符。如果是二元运算符,左操作数是对象本身的数据,由this指针指出,右操作数则需要通过运算符重载函数的参数表来传递;如果是一元运算符,操作数由对象的this指针给出,就不再需要任何参数。
对于二元运算符B,如果要重载为成员函数,实现表达式oprd1 B oprd2,其中oprd1是A类对象,则应该把B重载为类A的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载之后,表达式oprd1 B oprd2就相当于oprd1.operator B(oprd2)。对于前置一元运算符,如符号(-)等,如果需要重载为类的成员函数,实现表达式U oprd,oprd为A类的对象,则U应当重载为A的成员函数,没有形参,调用时相当于oprd.operator U()。对于后置运算符“++”和“--”,如果将它们重载为成员函数,用来实现oprd++,oprd--,其中oprd为A类对象,那么就重载为A类的成员函数,这时函数需要带一个int型形参,用以区别前置++,--运算符,重载之后相当于函数调用oprd.operator ++(0),oprd.operator --(0).这里的int型参数在运算中不起任何作用。如果运算符重载为非成员函数,那么运算所需要的操作数都需要通过函数的形参表来传递,在形参表中的形参从左至右的顺序就是运算符操作数的顺序.如果需要访问运算符参数对象的私有成员,可以将该函数声明为类的友元函数。
对于二元运算符B,如果要实现oprd1 B oprd2,其中oprd1和oprd2只要有一个具有自定义类型,就可以将B重载为非成员函数,函数的形参为oprd1和oprd2。oprd1 B oprd2就相当于operator B(oprd1,oprd2)。对于前置一元运算符U,如果要实现U oprd,其中oprd具有自定义类型,就可以将U重载为非成员函数,函数的形参为oprd。相当于调用operator U(oprd)。 对于后置运算符++和--,如果要实现oprd++和oprd--,其中oprd为自定义类型,那么运算符就可以重载为非成员函数,形参有两个,一个是oprd,一个是int。运算符的两种重载形式各有千秋,成员函数的重载方式更加方便,但有时出于以下原因,需要使用非成员函数的重载方式:
要重载的操作符的第一个操作数不是可以改变的类型,比如<<和>>运算符的第一个操作数是ostream类型的引用,是标准库的类型,无法向其添加成员函数。以非成员函数的方式重载,支持更灵活的类型转换。因为以非成员函数重载的操作符,它的左右操作数都可以就行隐式类型转换(通过构造函数),而以成员函数重载的运算符,左操作数必须是自定义类的对象,无法对它进行类型转换,所以只有右操作数可以进行隐式类型转换。举个例子说明第2点:
重载为非成员函数:
class Direction{ //省略细节 }; class Vector{ public: Vector(const Direction& ,double mangnitude = 1.0); }; Vector operator -(cosnt Vector&); main(){Direction d;Vector v = -d; //合法:operator-(Vector(d))隐式类型转换}重载为成员函数:
class Direction{ //省略细节 }; class Vector{ public: Vector(const Direction& ,double mangnitude = 1.0); Vector operator -(); }; main(){ Direction d; Vector v = -d; //编译错误,不可以进行隐式类型转换 } 在C++ Strategies and tactics一书中,作者认为应该禁止隐式转换,因为这会导致代码难以维护和理解,因此,在可能的情况下,最好将一元操作符实现为成员函数。对于 = ,[] , () , -> 这四个操作符必须实现为成员函数。
对于其它二元操作符来说,是否将它们实现为成员函数取决于我们是否要对左操作数进行隐式转换:
对于赋值类操作符(如+=)来说,我们希望的是禁止对左操作数进行隐式转换,如Complex c; c += 5;我们很难想象c在+=的操作实施前就被转换成其它东西。如果上面的代码顺利通过编译,在它运行后c的值不会改变,这一点让人疑惑。如果我们将赋值类的操作符重载为成员函数的话就可以杜绝这种情况,并且保证它们和operator =有一致的行为。对于那些非赋值类操作符来说,禁止对左操作数进行隐式转换,但却允许对右操作数进行隐式转换同样让人迷惑。如 class Complex{ //此处忽略细节 public: Complex(double = 0.0,double = 0.0); Complex operator+(const Complex&)const; }; main(){ Complex c(1.0); Complex d = c + 1.0;//合法:对右操作数进行隐式转换,Complex(1.0,0.0),其中0.0是默认值 Complex e = 1.0 + c;//编译器错误:编译器不会对成员函数的左操作数隐式转换,所以没有合适的声明对应这次函数调用 }操作符建议所有一元操作符成员= [] () ->必须是成员+= -= /= *= ^= &= != ~= %= >>= <<=成员所有其它二元操作符非成员 重载为成员函数可已禁止编译器对第一个操作数(左操作数)进行隐式转换重载为非成员函数可以确保在有隐式类型转换的情况下,两个操作数都可以以同样的方式进行操作
一个函数有四种返回值类型:
l 返回一个一般的值:T f();l 返回一个常量值:const T f();l 返回一个引用:T& f();l 返回一个常引用:const T& f();虽然何时采用哪种返回值并无明确规定,却有一定的规律可循:
如果返回值是简单类型,如int,double,char等,不需要用const修饰。如果希望返回值可以作为左值,则必须返回一个引用;反之则返回一般的值。类类型不是简单数据类型,接下来讨论。
是否返回一个引用取决于返回的对象是否要作为左值:如果希望作为左值,就返回引用类型。
返回一个局部变量(或其它生命周期短的变量)的引用会出现问题,不论是否使用了const修饰符,因此要避免返回一个局部变量的引用。返回一个局部变量会调用它的复制构造函数构建一个临时对象。如果不能确定到底是用const T&还是const T,那就用const T作为返回值类型比较安全,虽然牺牲一点效率。
如果一个类的公有成员函数返回一个类的私有成员变量,必须用const修饰返回值。
总结:
l 返回一个一般的值:这种返回值不能作为左值,此外返回值可以直接调用相关的成员函数修改返回值,如f().mutator()。返回值会调用复制构造函数。l 返回一个常量值:与前一种情况几乎相同,唯一不同的地方是返回值不能通过调用成员函数进行修改返回值。l 返回一个引用:这种返回值类型可以作为左值,而且可以通过成函数修改返回值,返回值不会调用复制构造函数。l 返回常引用:这种返回值不能作为左值,且不能通过成员函数修改返回的对象。返回值不调用复制构造函数。相反,返回临时对象的方式是完全不同的。编译器会直接把这个对象创建在外部返回值的内存单元,仅仅需要调用构造函数即可。这种方法称为返回值优化。
赋值运算符必须重载为成员函数,因为如果没有为类定义一个operator=的话,编译器就会自动为类创建一个默认的版本,如果类中没有动态创建的资源时,这个默认的版本,通常可以很好的工作:它会把一个对象的所有的数据的值赋值给新创建的这个新对象的相应变量,但是当类中存在一些动态分配的资源时(如指针等),这样直接赋值的方法就危险了,因为会有多个指针指向同一块内存,当通过一个对象改变这处内存时也会影响别的对象,更危险的是如果某个对象释放了这个内存,那么别的对象就不能再引用,这会容易发生错误。因此,对于类中包含动态变量时,要自己定义赋值运算符。
实际上,当类中包含指针时,我们需要定义四个函数:所有必需的普通构造函数,拷贝构造函数,operator=和析构函数。
在定义operator=时要注意检查自赋值,即检查this指针与模板对象的地址。避免对象对自身赋值。
通常是这么定义的:
class A{ //省略 public: //... A& operator=(const A& rhs){ //...做一些分配空间和赋值的工作 return *this; } };下标运算符[]必须是成员函数并且只接受一个整型参数。因为它必须是类的对象调用,所以必须是成员函数。
这个运算符返回一个引用,可以方便的作为左值
当希望一个对象表现的像一个指针时,通常就需要用到operator->。由于这样的对象比一个一般的指针有着更多的灵巧性,因此常被称作灵巧指针(smart pointer)。如果想用类包装一个指针以使指针更加安全,或是在迭代器(iterator)普通用法中,这样做会特别有用。迭代器是一个对象,这个对象可以作用于其他对象的容器或集合上,每次选择它们中的一个,而不用提供对容器的直接访问。
指针间接引用运算符一定是一个成员函数。他有额外的非典型的限制:
它必须返回一个对象(或对象的引用),该对象也有一个指针间接引用运算符或者必须返回一个指针,被用于选择指针间接引用符箭头所指的内容例子:
#include <iostream> #include <vector> using namespace std; class Obj{ static int i,j; public: void f()const{cout<<i++<<endl;} void g()const{cout<<j++<<endl;} }; int Obj::i = 48; int Obj::j = 11; //container class ObjContainer{ vector <Obj*> a; public: void add(Obj*obj){ a.push_back(obj); } friend class SmartPointer; }; class SmartPointer{ ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc):oc(objc),index(0){} //return value indicate end of list: bool operator++(){//prefix if(index >= oc.a.size())return false; if(oc.a[++index] == 0)return false; return true; } bool operator++(int){//postfix return operator ++();//use prefix version } Obj* operator->()const{ return oc.a[index]; } }; int main(int argc, char *argv[]) { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz;i++) oc.add(&o[i]); SmartPointer sp(oc);//创建一个迭代器 do{ sp->f(); sp->g(); }while(sp++); } 本示例主要使用了三个类:Obj类定义了程序中使用的一些对象,ObjContainer 类相当于一个容器,保存了若干指向Obj对象的指针,但却不能取回这些指针,SmartPointer类被声明为ObjContainer 的友元类,所以它允许进入这个容器内,SmartPointer可以使用运算符++向前移动它。注意:SmartPointer是和所创建的容器配套使用的,不存在一个通用的灵巧指针,同理,迭代器也是。
尽管sp没有f()和g(),但重载的->操作符返回的Obj*会调用那些函数