运算符重载
C++类特性丰富、复杂、功能强大。
学习C++的难点之一是要记住大量的东西,但在拥有丰富的实践经验前,是很难全部记住这些东西的。学习这种语言最好的方法,就是在开发自己的C++程序时,使用其中的新特性。对这些新特性有了充分的认识和了解之后再去添加其他的C++特性。
下面介绍一种使对象操作更美观的操作。
运算符重载是一种C++多态,是C++使用户能够定义多个名称相同但是特征标(参数列表)不同的函数。这被称为函数重载或函数多态,旨在让用户可以使用同名的函数来完成相同的基本操作。
想象一些,对于相同的操作,但是对于不同的物体,一定要用不同的英文单词,那会多么笨拙(抬起脚:lift_foot,抬起汤勺:lift_sp,对于抬起,我们希望只用一个单词lift就表示所有对象的抬起动作)运用运算符重载将重载的概念扩展到运算符上,赋予运算符多重含义。
实际上(包括在C语言里)已经有许多运算符被重载,例如*
可以是间接寻址运算符,也可以是乘法运算符。C++允许重载扩展到用户定义的类型,例如允许用+
将两个对象相加。重载运算符可以让代码看起来更自然。例如将两个同类型的数组相加是一种很常用的运算,通常需要用到如下for循环:1
2for(int i = 0; i < 20; i++)
c[i] = a[i] + b[i];
但在C++中,如果定义了数组的类,可以重载+运算符,可以有这样的语句:1
c = a + b;
这样简单的加法隐藏了内部机理,强调了操作实质,是OOP的另一个目标。当然,C++也有对运算符重载做出了一些限制。
常规方法
设计一个 Time 类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Time
{
private:
int hours;
int minutes;
public:
Time(){hours = 0; minutes = 0;}
Time(int h, int m = 0);
Time Sum(const Time & t)const;
};
Time Time::Sum(const Time & t)const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
将Time类转换为重载的加法运算符,只要把Sum()
的名称改为 operator+()
。即:把运算符(这里是加号)放到operator的后面,然后将结果用作方法名即可。
1 | Time operator+(const Time & t) const; |
注意参数是引用,目的是为了提高效率。如果按值传递Time对象,代码功能相同,但传递引用速度将更快,使用内存更少。
注意返回值不能是引用,因为函数将在函数内创建一个新的对象sum,如果返回类型是Time &,则引用的将是sum对象,但sum是局部变量,函数结束时将删除,因此引用将指向一个不存在的对象。不要返回指向局部变量或临时变量的引用!
和Sum()一样,operator+()也是由Time对象调用的,
它会把第二个Time对象作为参数,返回一个Time对象。
首先可以像Sum()一样调用operator+()方法:1
c = a.operator+(b);
但方法命名为operator+()之后,可以使用运算符表示法:1
c = a + b;
运算符左侧的对象(这里是a)是调用对象,右边的对象(这里是b)是作为参数被传递的对象。
编译器将根据操作数的类型来决定如何做。1
2
3
4int a, b, c;
Time A, B, C;
c = a + b; //int型加法
C = A + B; //加号被认为是Time对象
可以这样做吗:1
t4 = t1 + t2 + t3;
为了回答这个问题,我们可以考虑转换成函数调用形式:1
2t4 = t1.operator+(t2 + t3);
t4 = t1.operator+(t2.operator+(t3));
t2.operator+(t3)
返回一个对象是t2和t3的和,再去作为t1.operator+()
的参数,就能返回t1、t2、t3之和,是我们想要的结果。
重载限制
多数C++运算符都可以以这样的方式重载。重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。下面详细介绍各种运算符限制重载:
- 重载后的运算符至少有一个操作数是用户定义的类型。这将防止用户为标准类型重载运算符。例如:不能将减法运算符重载为计算两个int值的和。这样确保程序可以正常运行。
- 使用运算符的时候不能违反原来的句法规则。例如不能将运算符重载为只使用一个操作数。
- 不能修改运算符的优先级。如果将加号运算符重载成两个类相加,则新的运算符和原来的加号具有相同优先级。
- 不能创建新的运算符,例如不能定义operator**()来求幂。
- 不能重载如下运算符:
运算符 | 含义 |
---|---|
sizeof | sizeof运算符 |
. | 成员运算符 |
.* | 成员指针运算符 |
:: | 作用域解析运算符 |
?: | 条件运算符 |
typeid | 一个RTTI运算符 |
const_cast | 强制类型转换运算符 |
dynamic_cast | 强制类型转换运算符 |
reienterpret_cast | 强制类型转换运算符 |
static_cast | 强制类型转换运算符 |
可重载的运算符(部分):
大多数运算符都可以通过成员或非成员进行重载,但以下的运算符只能通过成员函数重载:
运算符 | 含义 |
---|---|
= | 赋值运算符 |
() | 函数调用运算符 |
[] | 下标运算符 |
-> | 通过指针访问类成员运算符 |
除了这些正式限制之外,还应该在使用运算符重载时遵循一些明智的限制:例如:不要把*重载为交换两个对象的数据,这种运算符的表示法并没有标明可以完成这样的工作。因此最好定义一个具有说明性的类方法:Swap()
(不要无脑重载运算符这样不吼啊!~)
友元函数
现在我们知道,C++会限制对类对象私有部分的访问。通常,公有类方法提供唯一的途径访问,但是有时候这种限制太严格,以致不适合特定的编程问题。在这种情况下,C++提供了另一种形式的访问权限:友元。友元有三种:
• 友元函数
• 友元类
• 友元成员函数
下面介绍友元函数,剩下的两种之后会介绍。通过让函数成为类的友元,可以赋予该函数与类成员函数相同的访问权限。
为何需要友元?
重载二元运算符(带两个参数的运算符)常常需要友元。例如Time对象的operator*()
重载就属于这种情况。乘法运算时将一个Time类对象与一个double值结合在一起,记住:运算符左侧是调用对象,右侧是参数。所以下面的语句:1
A = B * 0.75;
是可行的,但是下面的语句呢?1
A = 0.75 * B;
从概念上说两个语句理应是一样的,但第二个表达式就不对应成员函数了,因为0.75不是Time类对象。如何实现让0.75 * B
这样的语句也能顺利实现乘法的功能呢?
有一个方法就是写新的非成员函数。1
2
3
4Time operator*(double m, const Time &t)
{
return t * m; //use t.operator*(m)
}
非成员函数不是对象调用的,所有参数都使用显示参数,写这样的函数就可以使A = 0.75 * B;
这种非友元函数语句能工作。但这么做也是有问题的,非成员函数不能直接访问类的私有数据。然而有一种特殊的非成员函数可以访问类私有成员,它们就是友元函数。
创建友元
友元函数的原型要放在类声明里,在原型声明前加上关键字friend:1
friend Time operator*(double m, const Time & t);
该原型意味着以下两点:
• 友元函数不是成员函数,不能使用成员运算符调用。
• 虽然不是成员函数,但与成员函数的访问权限相同。
编写函数定义时,因为它不是成员函数,请不要使用Time::限定符,不要添加关键词friend。如下:1
2
3
4
5
6
7
8
9Time operator*(double m, const Time &t)
{
Time result;
long totalminutes = t.hours* m * 60 + t.minutes*mult;
result.minutes = totalminutes / 60;
result.hours = totalminutes % 60;
return result;
}
友元是否有悖于OOP?
乍一看友元违背了OOP数据隐藏的原则,实际上这个观点太片面了。应该将友元看做类的扩展接口的组成部分。例如,double乘以Time和Time乘以double概念是完全相同的,但前一个要求必须使用友元函数,这只是C++句法的区别。
总之,类方法和友元只是表达类接口的两种不同机制。
重载 << 运算符
一个很有用的类特性:对<<
进行重载,使之能与cout
一起来用于显示对象的内容。假如有一个Time对象a,我们设计的显示对象内容类方法是show()
。然而,如果能像下面这样显示,会更好的吧:
1 | cout << a; |
<<
也是一个能被重载的运算符。实际上它已经被重载过很多次了,在原本的C语言里它是位运算符(左移)。ostream
将其重载成为一个输出工具。
tips:cout是一个ostream对象,它是智能的,能识别所有的C++基本类型,是因为对于每种类型,ostream类声明都包含了相对应的<<重载定义。所以为了让cout能识别Time对象,我们可以对Time类声明进行修改来让Time类知道如何使用cout。
重载版本1
必须使用友元函数。原因是(还是那句话)运算符左侧是调用对象,右侧是参数。cout
是ostream
类对象不是Time类对象,如果不使用友元,则必须这样使用<<
:1
a << cout; //很怪异对不对。。。
因此,第一个重载版本出来了:1
2
3
4
5
6friend void operator<<(ostream & os, const Time &t);
void operator<<(ostream & os, const Time &t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
}
调用cout应该调用cout本身,而不是他的拷贝,因此函数按引用而不是按值传递该对象。表达式cout << a
将导致os是cout的一个别名。
重载版本2
版本1有一个问题就是不允许像通常那样将重新定义的<<运算符于cout一起使用:1
cout<<"a time: "<< a << endl; //can't do
要理解为什么这样做不行的原因是需要了解一些关于cout操作的知识,看下面的代码:
1 | int a = 5; |
其实第三行相当于
1 | (cout << a) << b; |
iostream
定义<<
运算符的左边是一个ostream对象
。首先cout
是ostream对象
,所以cout << a
是符合要求的,然后(cout << a)
这个整体也在一个<<
运算符的左边,所以也要求这个整体也是一个ostream对象
。因此,ostream
类把operator<<()
函数的返回设置为一个指向ostream对象的引用。所以据此我们也对自己的函数进行修改,让其返回对ostream对象的引用即可。
第二个重载版本如下:1
2
3
4
5
6
7friend std::ostream & operator<<(ostream & os, const Time &t);
std::ostream & operator<<(ostream & os, const Time &t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
return os;
}
二义性:使用成员函数还是非成员函数?
加法运算符重载函数声明:
1.类成员函数重载加法:1
Time operator+(const Time & t)const;
2.友元函数重载加法:1
friend Time operator+(const Time & t1, const Time & t2);
差别:
- 对于成员函数来说,一个操作数通过this指针隐式传递,另一个操作数作为函数参数显示传递。
- 对于友元函数来说,两个操作数都是作为参数传递。
编译器会将下面的语句:1
T1 = T2 + T3;
分别对应转换为:1
2T1 = T2.operator+(T3);
T1 = operator+(T2,T3);
所以,在定义运算符时,必须选择其中一种格式,而不能同时选择这两种格式,因为这两种格式都与同一个表达式匹配,同时定义会导致二义性错误。
到底哪个好呢?对于某些运算符来说,成员函数是唯一合法的选择(见重载限制)。在其他情况下,没有太大差别。有时根据类设计,非成员函数版本可能更好。
一个重载练习:矢量类
可复制编译。
1 | // vect.h |