重载运算符的规则
- l 运算符重载时,该运算符包含的参数必须至少有一个类类型。
- l 大多数运算符都有三种重载方式,以成员函数,以非成员函数(普通函数),以友元函数进行重载。
- l 下面几个运算符只能以成员函数进行重载:=、 [ ]、->、和()。
- l 只能重载已有的运算符,不能创建新的运算符。
- l 一元运算符在重载后仍是一元运算符,二元运算符重载后仍是二元运算符
- l 运算符重载不会改变优先级
- l 点运算符(.),作用域限制符(::),sizeof,三元运算符(?:),及(.*)都不能重载
- l 重载的运算符不能包含默认参数
在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); };
main(){Direction d;Vector v = -d; //合法:operator-(Vector(d))隐式类型转换}Vector operator -(cosnt Vector&);
重载为成员函数:
class Direction{ //省略细节 }; class Vector{ public: Vector(const Direction& ,double mangnitude = 1.0);
Vector operator -();};main(){
Direction d;
Vector v = -d; //编译错误,不可以进行隐式类型转换
} 在C++ Strategies and tactics一书中,作者认为应该禁止隐式转换,因为这会导致代码难以维护和理解,因此,在可能的情况下,最好将一元操作符实现为成员函数。对于 = ,[] , () , -> 这四个操作符必须实现为成员函数。
对于其它二元操作符来说,是否将它们实现为成员函数取决于我们是否要对左操作数进行隐式转换:
- 对于赋值类操作符(如+=)来说,我们希望的是禁止对左操作数进行隐式转换,如
我们很难想象c在+=的操作实施前就被转换成其它东西。如果上面的代码顺利通过编译,在它运行后c的值不会改变,这一点让人疑惑。如果我们将赋值类的操作符重载为成员函数的话就可以杜绝这种情况,并且保证它们和operator =有一致的行为。Complex c; c += 5;
- 对于那些非赋值类操作符来说,禁止对左操作数进行隐式转换,但却允许对右操作数进行隐式转换同样让人迷惑。如
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 返回常引用:这种返回值不能作为左值,且不能通过成员函数修改返回的对象。返回值不调用复制构造函数。
返回值优化(return value optimization)
乍看起来这像是调用一个构造函数,其实并非如此。这是临时对象语法,它是在说:“创建一个临时Integer对象并返回它”。这跟以下的代码是不同的class Integer{ long i; public: //其余省略 firend const Integer operator+(const Integer &left,const Integer &right); }; const Integer operator+(const Integer &left,const Integer &right){ return Integer(left.i+right.i); }
上述代码会发生三件事:1,调用构造函数创建tmp对象;2、拷贝构造函数把tmp拷贝到外部返回值的存储单元里;3、当tmp在作用域的结尾时调用析构函数。Integer tmp(left.i + right.i); return tmp;
相反,返回临时对象的方式是完全不同的。编译器会直接把这个对象创建在外部返回值的内存单元,仅仅需要调用构造函数即可。这种方法称为返回值优化。
几个特殊的运算符
operator=
赋值运算符必须重载为成员函数,因为如果没有为类定义一个operator=的话,编译器就会自动为类创建一个默认的版本,如果类中没有动态创建的资源时,这个默认的版本,通常可以很好的工作:它会把一个对象的所有的数据的值赋值给新创建的这个新对象的相应变量,但是当类中存在一些动态分配的资源时(如指针等),这样直接赋值的方法就危险了,因为会有多个指针指向同一块内存,当通过一个对象改变这处内存时也会影响别的对象,更危险的是如果某个对象释放了这个内存,那么别的对象就不能再引用,这会容易发生错误。因此,对于类中包含动态变量时,要自己定义赋值运算符。
实际上,当类中包含指针时,我们需要定义四个函数:所有必需的普通构造函数,拷贝构造函数,operator=和析构函数。
在定义operator=时要注意检查自赋值,即检查this指针与模板对象的地址。避免对象对自身赋值。
通常是这么定义的:
class A{ //省略 public: //... A& operator=(const A& rhs){ //...做一些分配空间和赋值的工作 return *this; } };
operator[]
下标运算符[]必须是成员函数并且只接受一个整型参数。因为它必须是类的对象调用,所以必须是成员函数。
这个运算符返回一个引用,可以方便的作为左值
operator->
当希望一个对象表现的像一个指针时,通常就需要用到operator->。由于这样的对象比一个一般的指针有着更多的灵巧性,因此常被称作灵巧指针(smart pointer)。如果想用类包装一个指针以使指针更加安全,或是在迭代器(iterator)普通用法中,这样做会特别有用。迭代器是一个对象,这个对象可以作用于其他对象的容器或集合上,每次选择它们中的一个,而不用提供对容器的直接访问。
指针间接引用运算符一定是一个成员函数。他有额外的非典型的限制:
- 它必须返回一个对象(或对象的引用),该对象也有一个指针间接引用运算符
- 或者必须返回一个指针,被用于选择指针间接引用符箭头所指的内容
例子:
本示例主要使用了三个类:Obj类定义了程序中使用的一些对象,ObjContainer 类相当于一个容器,保存了若干指向Obj对象的指针,但却不能取回这些指针,SmartPointer类被声明为ObjContainer 的友元类,所以它允许进入这个容器内,SmartPointer可以使用运算符++向前移动它。#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++); }
注意:SmartPointer是和所创建的容器配套使用的,不存在一个通用的灵巧指针,同理,迭代器也是。
尽管sp没有f()和g(),但重载的->操作符返回的Obj*会调用那些函数