运算符重载是一种形式的C++多态
要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数的格式如下:
operatorop(atgument-list)
例如,operator+()重载+运算符,operator*()重载*运算符。op必须是有效的C++运算符,不能虚构一个新的运算符
一个运算符重载示例
Time Time::operator+(const Time & t) const //const表明函数不会更改调用对象
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
这两种表示方法等价:
total = coding.operator+(fixing);//函数调用
total = coding + fixing;//运算符重载
这两种表示法都将调用operat+()方法,注意:在运算符表示法中,运算符左侧的对象(这里为coding)是调用对象(隐式参数),运算符右边的对象(这里是fixing)是作为参数被传递的对象(显式参数)
编译器将根据操作数的类型来确定如何做:
int a,b,c;
Time A,B,C;
c = a + b; //使用int的+号
C = A + B; //用运算符重载调用
可以将两个以上的对象相加,如:
t4 = t1 + t2 + t3;
由于+是从左向右结合的运算符,因此上述语句首先被转换成下面这样:
t4 = t1.operator+(t2 + t3);
然后,函数参数本身被转换成一个函数调用,结果如下:
t4 = t1.operator+(t2.operator+(t3));
函数调用t2.operator+(t3)返回一个Time对象,后者是t2和t3的和,然而,该对象称为函数调用t1.operator+()的参数,该调用返回t1与t2和t3之和的Time对象的和
重载限制
重载的运算符不必是成员函数,但必须至少有一个操作数是用户自定义的类型下面详细介绍C++对用户定义的运算符重载的限制
1、重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符,不能将减法运算符(-)重载为计算两个double值的和,而不是它们的差
2、使用运算符时不能违反运算符原来的句法规则,例如,不能将求模运算符(%)重载成使用一个操作数,同样,不能修改运算符的优先级
3、不能创建新的运算符,例如,不能定义operator**()函数来求幂
4、不能重载下面的运算符:
- sizeof:sizeof运算符
- .:成员运算符
- .*:成员指针运算符
- :::作用域解析运算符
- ?::条件运算符
5、大多数运算符可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载
- =:赋值运算符
- ():函数调用运算符
- []:下标运算符
- ->:通过指针访问类成员的运算符
友元
友元有3种:
- 友元函数
- 友元类
- 友元成员函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限
在前面的示例中,加法运算符结合两个Time值,而乘法运算符将一个Time值与一个double值结合在一起,这限制了运算符的使用方式,记住,左侧的操作数是调用对象,也就是说,下面的语句:
A = B * 2.75;
将被转换为下面的成员函数调用:
A = B.operator*(2.75);
但下面的语句有如何呢
A = 2.75 * B;
从概念上说,2.75 * B应与B * 2.75相同,但第一个表达式不对应与成员函数,因为2.75不是Time类型对象,记住,左侧的操作数应是调用对象
解决这个难题的一种方式是,告知每个人,只能按B * 2.75这种格式编写,不能写成2.75 * B
然而,还有另一种解决方式–非成员函数,记住,大多数运算符都可以通过成员或非成员函数来重载,非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数,这样编译器能将下面的表达式:
A = 2.75 * B;
与下面的函数调用匹配
A = operator*(2.75, B);
该函数的原型如下:
Time operator*(double m, const Time & t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数
但引发了一个问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问,然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数
创建友元
创建友元函数的第一步是将其原型放在类声明中,并在原型声明前面加上关键字friend:
friend Time operator*(double m, const Time & t);
该原型意味着两点:
- 虽然operator*()函数是在勒声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
- 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同
第二步是编写函数定义,不要在函数定义中加关键字friend,定义如下:
Time operator*(double m, const Time & t)
{
Time result;
long totalminutes = t.hours * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
有了上述声明和定义后,下面的语句:
A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数
A = operator*(2.75, B);
实际上,下面的方式对定义进行修改,可以将这个友元函数编写为非友元函数:
Time operator*(double m, const Time & t)
{
return t * m;
}
原来的版本显式地访问t.minutes和t.hours,所以它必须是友元,这个版本将Time对象t作为一个整体使用,让成员函数来处理私有值,因此不必是友元
常用的友元:重载<<运算符
假设trip是一个Time对象,为显式Time的值,前面使用的是Show(),然而,如果可以像下面这样操作将更好:
cout<<trip;
之所以可以这样做,是因为<<是可被重载的C++运算符之一
1、<<的第一种重载版本
要使Time类知道使用cout,必须使用友元函数,因为下面这样的语句使用两个对象:
cout<<trip;
如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数,就像使用成员函数重载*运算符那样
trip<<cout;
这样会令人迷惑,但通过使用友元函数,可以像下面那样重载运算符:
void operator<<(ostream & os, const Time & t)
{
os<<t.hours<<" hours, "<<t.minutes<<" minutes";
}
这样可以使用下面的语句:
cout<<trip;
2、<<的第二种重载版本
前面介绍的实现存在一个问题,像下面这样的语句可以正常工作:
cout<<trip;
但这种实现不允许像通常那样将重新定义的<<运算符与cout一起使用
cout<< "Trip time: " << trip << " (Tuesday)\n"; //不可行
要理解这样做不可行的原因以及如何做才能使其可行,首先要了解关于cout操作的一点知识
int x = 5;
int y = 8;
cout << x << y;
C++从左至右读取输出语句,意味着它等同于:
(cout << x) << y;
正如iostream中定义的那样,<<运算符要求左边是一个ostream对象,因此,ostream类将operator<<()函数实现为返回一个指向ostream对象的引用,具体的说,它返回一个指向调用对象(这里是cout)的引用,因此,表达式(cout<<x)本身就是ostream对象cout,从而可以位于运算符的左侧
可以对友元函数采用相同的方法,只要修改operator<<()函数,让它返回ostream对象的引用即可:
ostream & operator<<(ostream & os, const Time & t)
{
os<<t.hours<<" hours, "<<t.minutes<<" minutes";
return os;
}
注意,返回类型是ostream&,这意味着该函数返回ostream对象的引用
只有在类声明中的原型才能使用friend关键字,除非函数定义也是原型,否则不能在函数定义中使用该关键字
重载运算符:作为成员函数还是非成员函数
一般来说,非成员函数应是友元函数,这样它才能直接访问类的私有数据,例如,Time类的加法运算符在Time类声明中的原型如下:
Time operator+(const Time & t) const; //作为成员函数
这个类也可以使用下面的原型:
friend Time operator+(const Time & t1, const Time & t2);
加法运算符需要两个操作数:
1、对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;
2、对于友元版本来说,两个操作数都作为参数来传递
这两个原型都与表达式T2 + T3匹配,其中T2和T4都是Time类型对象,也就是说,编译器将下面的语句:
T1 = T2 + T3;
转换为下面两个的任何一个:
T1 = T2.operator+(T3); //成员函数版本
T1 = operator+(T2, T3); //友元函数版本
记住,在定义运算符时,必须选择其中的一种格式,而不能同时选择这两种格式,因为这两种格式都与同一个表达式匹配,同时定义这两种格式将视为二义性错误,导致编译错误
对已重载的运算符进行重载
可以对已重载的运算符进行重载,例如,在C++中,-运算符有两种含义:首先,它是减法运算符,是二元运算符,因为它有两个操作数;其次,使用一个操作数时,它是负号运算符,是一元运算符,只有一个操作数,如:
Vector operator-(const Vector & b) const; // 原型
Vector Vector::operator-(const Vector & b) const
{
return Vector(x - b.x, y - b.y);
}
操作数的顺序非常重要,下面的语句:
diff = v1 - v2;
将被转换为下面的成员函数调用:
diff = v1.operator-(v2);
这意味着将从隐式矢量参数减去以显式参数传递的的矢量,所以应使用x-b.x而不是b.x-x
接下来,来看一元负号运算符,它只使用一个操作数,将这个运算符用于数字(如-x)时,将改变它的符号
下面是重载负号的原型和定义:
Vector operator-() const;
Vector Vector::operator-() const
{
return Vector (-x, -y);
}
现在,operator-()有两种不同的定义,这是可行的,因为它们的特征标不同,可以定义-运算符的一元和二元版本,因为C++提供了该运算符的一元和二元版本,对于只有二元形式的运算符(如除法运算符),只能将其重载为二元运算符
因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符数量与相应的内置C++运算符相同,就可以多次重载同一个运算符