C++学习之运算符重载

运算符重载

C++类特性丰富、复杂、功能强大。

学习C++的难点之一是要记住大量的东西,但在拥有丰富的实践经验前,是很难全部记住这些东西的。学习这种语言最好的方法,就是在开发自己的C++程序时,使用其中的新特性。对这些新特性有了充分的认识和了解之后再去添加其他的C++特性。

下面介绍一种使对象操作更美观的操作。

运算符重载是一种C++多态,是C++使用户能够定义多个名称相同但是特征标(参数列表)不同的函数。这被称为函数重载函数多态,旨在让用户可以使用同名的函数来完成相同的基本操作。

想象一些,对于相同的操作,但是对于不同的物体,一定要用不同的英文单词,那会多么笨拙(抬起脚:lift_foot,抬起汤勺:lift_sp,对于抬起,我们希望只用一个单词lift就表示所有对象的抬起动作)运用运算符重载将重载的概念扩展到运算符上,赋予运算符多重含义。

实际上(包括在C语言里)已经有许多运算符被重载,例如*可以是间接寻址运算符,也可以是乘法运算符。C++允许重载扩展到用户定义的类型,例如允许用+将两个对象相加。重载运算符可以让代码看起来更自然。例如将两个同类型的数组相加是一种很常用的运算,通常需要用到如下for循环:

1
2
for(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
19
class 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
2
3
4
5
6
7
8
9
10
Time operator+(const Time & t) const;

Time Time::operator+(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,如果返回类型是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
4
int a, b, c;
Time A, B, C;
c = a + b; //int型加法
C = A + B; //加号被认为是Time对象

可以这样做吗:
1
t4 = t1 + t2 + t3;

为了回答这个问题,我们可以考虑转换成函数调用形式:
1
2
t4 = t1.operator+(t2 + t3);
t4 = t1.operator+(t2.operator+(t3));

t2.operator+(t3)返回一个对象是t2和t3的和,再去作为t1.operator+()的参数,就能返回t1、t2、t3之和,是我们想要的结果。

重载限制

多数C++运算符都可以以这样的方式重载。重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。下面详细介绍各种运算符限制重载:

  1. 重载后的运算符至少有一个操作数是用户定义的类型。这将防止用户为标准类型重载运算符。例如:不能将减法运算符重载为计算两个int值的和。这样确保程序可以正常运行。
  2. 使用运算符的时候不能违反原来的句法规则。例如不能将运算符重载为只使用一个操作数。
  3. 不能修改运算符的优先级。如果将加号运算符重载成两个类相加,则新的运算符和原来的加号具有相同优先级。
  4. 不能创建新的运算符,例如不能定义operator**()来求幂。
  5. 不能重载如下运算符:
运算符 含义
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
4
Time 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
9
Time 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

必须使用友元函数。原因是(还是那句话)运算符左侧是调用对象,右侧是参数。coutostream类对象不是Time类对象,如果不使用友元,则必须这样使用<<:

1
a << cout; //很怪异对不对。。。

因此,第一个重载版本出来了:

1
2
3
4
5
6
friend 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
2
3
int a = 5;
int b = 8;
cout << a << b;

其实第三行相当于

1
(cout << a) << b;

iostream定义<<运算符的左边是一个ostream对象。首先coutostream对象,所以cout << a是符合要求的,然后(cout << a)这个整体也在一个<<运算符的左边,所以也要求这个整体也是一个ostream对象。因此,ostream类把operator<<()函数的返回设置为一个指向ostream对象的引用。所以据此我们也对自己的函数进行修改,让其返回对ostream对象的引用即可。

第二个重载版本如下:

1
2
3
4
5
6
7
friend 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);

差别:

  1. 对于成员函数来说,一个操作数通过this指针隐式传递,另一个操作数作为函数参数显示传递。
  2. 对于友元函数来说,两个操作数都是作为参数传递。

编译器会将下面的语句:

1
T1 = T2 + T3;

分别对应转换为:
1
2
T1 = T2.operator+(T3);
T1 = operator+(T2,T3);

所以,在定义运算符时,必须选择其中一种格式,而不能同时选择这两种格式,因为这两种格式都与同一个表达式匹配,同时定义会导致二义性错误

到底哪个好呢?对于某些运算符来说,成员函数是唯一合法的选择(见重载限制)。在其他情况下,没有太大差别。有时根据类设计,非成员函数版本可能更好。

一个重载练习:矢量类

可复制编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// vect.h
#include<iostream>
#ifndef VECTOR_H_
#define VECTOR_H_
namespace VECTOR
{
class Vector
{
public:
enum Mode { RECT, POL };//RECT:直角坐标系,
// POL:极坐标系。
private:
double x; //横坐标x
double y; //纵坐标y
double mag; //向量的长度
double ang; //角度
Mode mode; //何种坐标系

//设定值的私有方法
void set_mag();
void set_ang();
void set_x();
void set_y();

public:
Vector();
Vector(double n1, double n2, Mode form = RECT);
void reset(double n1, double n2, Mode form = RECT);
~Vector();
//用于报告各种值的内联函数
double xval()const { return x; }
double yval()const { return y; }
double magval()const { return mag; }
double angval()const { return ang; }
void polar_mode();
void rect_mode();

//运算符重载
Vector operator+(const Vector & b)const;
Vector operator-(const Vector & b)const;
Vector operator-()const;
Vector operator*(double n)const;

//友元
friend Vector operator*(double n, const Vector & a);
friend std::ostream &
operator<<(std::ostream & os, const Vector & v);
};
}//end namespace VECTOR
#endif

//vect.cpp -- 方法定义
#include<cmath>
//#include "vect.h"
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;

namespace VECTOR
{
//斜率转换到角度,粗略值为57.2957795130823
const double Rad_to_deg = 45.0 / atan(1.0);

//直角坐标转极坐标
void Vector::set_mag()
{
mag = sqrt(x*x + y*y);
}

void Vector::set_ang()
{
if (x == 0.0&&y == 0.0)
ang = 0.0;
else
ang = atan2(y, x);
}

//极坐标转直角坐标
void Vector::set_x()
{
x = mag*cos(ang);
}

void Vector::set_y()
{
y = mag*sin(ang);
}

//公有方法
Vector::Vector()//默认构造函数
{
x = y = mag = ang = 0.0;
mode = RECT;
}

//当选择特定坐标系时的构造函数
Vector::Vector(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector()";
cout << "Vector set to 0.\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}

void Vector::reset(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector()";
cout << "Vector set to 0.\n";
x = y = mag = ang = 0.0;
mode = RECT;
}
}

Vector::~Vector()//析构函数
{

}

//切换坐标系
void Vector::polar_mode()
{
mode = POL;
}

void Vector::rect_mode()
{
mode = RECT;
}

//运算符重载
Vector Vector::operator+(const Vector & b)const
{
return Vector(x + b.x, y + b.y);
}

Vector Vector::operator-(const Vector & b)const
{
return Vector(x - b.x, y - b.y);
}

Vector Vector::operator-()const
{
return Vector(-x, -y);
}

Vector Vector::operator*(double n)const
{
return Vector(n*x, n*y);
}

//友元函数
Vector operator*(double n, const Vector &a)
{
return a*n;
}

std::ostream & operator<<(std::ostream & os, const Vector &v)
{
if (v.mode == Vector::RECT)
os << "(x,y) = (" << v.x << "," << v.y << ")";
else if (v.mode == Vector::POL)
{
os << "(m,a)=(" << v.mag << "," << v.ang*Rad_to_deg << ")";
}
else
os << "Vector object mode is invalid";
return os;
}
}//end namespace VECTOR