c++学习记录三:操作符函数

(本文来自《c++简明教程》)


完成了前两章的学习之后,我们应该掌握了如何写一个自己的类,使其能像标准数据类型那样使用。
但是,相较于真正的标准数据类型,目前写的类还存在一些不足。对于int,float,double和char等标准数据类型,它们最重要的一个特定就是,可以直接对它们进行操作。事实上,没有这些操作符,很难在c++中执行任何运算。
c++允许你为自己的类的对象定义如何执行这些运算(比如加,减,乘,除)。还可以为类的对象定义一个“测试相等性”操作符,以便判断两个对象是否“相等”。如此依赖,你的类才会变得真的与基本数据类型相似。


类操作符函数入门

类操作符函数的基本语法是相当简单的。一旦你掌握了这种写法,就可以为自己的类定义任意数量的操作符。
return_type operator@(argument_list)
使用上述语法时,需要将符号@替换一个有效的C++操作符,比如+,-,*和/。但是,你能定义的操作符并非只有4个。事实上c++标准类型支持的任何操作符都能在这里使用。
可以将一个操作符函数定义为成员函数或者全局函数(即“非成员函数”)。

  • 如将一个操作符函数定义为成员函数,左操作数就是该“操作符函数”的调用函数,右操作数则是该“操作符函数”的参数。
  • 如将一个操作符函数声明为全局函数,那么两个操作数都是该“操作符函数”的参数。

用一个例子来说明这个问题。下例在Point类中声明了加和减(+,-)操作符函数:

1
2
3
4
5
6
class Point {
//...
public:
Point operator+(Point pt);
Point operator-(Point pt);
};

有了上述声明后,就可以为Point对象使用操作符:

1
2
Point point1, point2, point3;
point1 = point2 + point3;

编译器在解释这个语句时,会调用左操作数(本例就是point2对象)的operator+函数。右操作数(本例就是point3对象)则成为传给该函数的一个参数。下图清楚地展示了这种关系:
操作符声明为成员函数,所以假如不加限定地使用 x 和 y,那么实际引用的就是point2的x和y的拷贝。可以在函数定义中看到这一特点的实际运用:

1
2
3
4
5
6
Point Point::operator+(Point pt) {
Point new_pt;
new_pt.x = x + pt.x;
new_pt.y = y + pt.y;
return new_pt;
}

未限定的数据成员x和y引用的是左操作数(本例就是point2对象)中的值。相反,进行了限定的pt.x和pt.y引用的是右操作数(本例就是point3对象)–或者说作为参数传递的那个对象–中的值。
这里的操作符函数的返回类型声明为Point:换言之,它们返回的是一个Point对象。这样做是有道理的:两个Point对象加到一起,自然应该得到另一个Point;类似地,如果从一个Point上减去一个Point,那么也应该得到另外一个Point。但要注意的是,C++允许你为一个操作符函数指定任何有效的返回值类型。
参数列表也可以包含任何类型。这里允许重载:可以声明两个同名的操作符函数,一个对int类型进行处理,另一个对double类型进行处理。
在Point类的情况下,有必要支持整数乘法运算。相应的操作符函数声明如下:
Point operator*(int n);
函数定义则为:

1
2
3
4
5
6
Point Point::operator*(int n) {
Point new_pt;
new_pt.x = x * n;
new_pt.x = y * n;
return new_pt;
}

同样地,函数在这里返回的是一个Point类型,虽然你也可以让它返回任何有效的类型。
作为一个相反的例子,你可以创建一个操作符函数来计算两个点之间的距离,并返回一个浮点(double)结果,为了计算这种“相对距离”,在此选择的操作符是%,但也可以选择c++定义的其他任何有效的二元操作符。这里要展示的一个重点在于。你可以为一种特定的运算返回任何类型的值:

1
2
3
4
5
6
#include <math.h>
double Point::operator%(Point pt) {
int d1 = pt.x - x;
int d2 = pt.y - y;
return sqrt((double) (d1 * d1 + d2 * d2));
}

基于上述函数定义,以下代码能正确打印两点之间的距离。在本例中,两个点分别是(20,20)和(24,23),它们的距离应为5.0。

1
2
3
Point pt1(20, 20);
Point pt2(24, 23);
cout<< "Distance between points is : "<< pt1%pt2;

操作符函数作为全局函数

上一节说过,还可以将操作符函数定义成全局函数。这样做有一个弊端,我们不能将所有相关的函数都集中到类的声明中。但是在某些情况下(稍后就会详细讨论),我们必须采用这种方法。
全局操作符函数是在所有类的外部声明的。参数列表中的类型决定了该函数支持哪一种操作数。例如,Point类的加法运算操作符函数就可以重写为一个全局函数。下面列出了函数声明(函数原型),它应该在调用该函数之前出现:
Point operator+(Point pt1, Point pt2);
下面是该函数的定义:

1
2
3
4
5
6
Point operator+(Point pt1, Point pt2) {
Point new_pt;
new_pt.x = pt1.x + pt2.x;
new_pt.y = pt1.y + pt2.y;
return new_pt;
}

下图展示了对这个函数的调用:
操作符声明为全局函数
现在两个操作符都被解释成函数参数。左操作数(本例就是point2对象)将它的值传给第一个参数pt1:右操作数(本例是point3对象)则将它的值传给第二个参数pt2。这这种情况下,不存在“调用对象”(即调用该函数的对象)的概念,而且Point数据成员的所有引用都必须限定。
这引发了一个问题。假如数据成员不是公共的,这个函数就不能访问它们。一个办法是使用类的取值和赋值函数(如果有的话)来访问数据:

1
2
3
4
5
6
7
Point operator+(Point pt1, Point pt2) {
Point new_pt;
int a = pt1.get_x() + pt2.get_x();
int b = pt1.get_y() + pt2.get_y();
new_pt.set(a, b);
return new_pt;
}

但这并不是一种优雅的解决方案,而且对于某些类来说,甚至根本不能这样写。例如,你的一个类可能完全不允许访问它的私有数据成员,但你仍然想为它写操作符函数。一种更好的方案是将函数声明为“友元函数”(friend function)。它意味着函数是公共的,但作为类的一个“朋友”,仍然能够访问类的私有成员。
下例为Point类声明了一个友元函数:

1
2
3
4
5
class Point {
//...
public:
friend Point operator+(Point pt1, Point pt2);
};

现在,函数定义能够直接访问Point类的所有成员,即使它们是private的。

1
2
3
4
5
6
7
8
Point operator+(Point pt1, Point pt2)
{
Point new_pt;
int a = pt1.x + pt2.x;
int b = pt1.y + pt2.y;
new_pt.set(a, b);
return new_pt;
}

有的时候,你必须将一个操作符函数写成全局函数。如果是成员函数书,那么在函数定义中,左操作数会被解释成该函数的“调用对象”。但是,假如左操作数不是一种对象类型呢?怎样支持下面这样的运算?
point = 3 * point2;
这里的问题在于,左操作数具有int类型,而非Point类型。但是,你不能将int当作一个类,为它写一种新运算。为了实现这样的运算,惟一的办法就是为操作符一个全局函数:

1
2
3
4
5
6
7
Point operator*(int n, Point pt)
{
Point new_pt;
new_pt.x = pt.x * n;
new_pt.y = pt.y * n;
return new_pt;
}

和前面一样,为了能访问私有数据成员,需要将函数设为类的一个友元:

1
2
3
4
5
class Point {
//...
public:
friend Point operator*(int n, Point pt);
};

下图展示了这个函数调用:
友元函数-p250


利用引用来提高效率

为了实现对象的数学运算,最容易想到的一种方式就是将简单对象类型(类)作为参数来传递。但正如前一章指出的那样,对象每次作为一个值来传递或返回时,都会调用拷贝构造函数。
除此之外,每次创建一个对象时。程序都必须从系统请求内存来容纳新建的对象。虽然所有这一切都是在后台自动进行的,但它肯定会多少影响程序的效率。
为了提供程序的效率,你可以在写一个类时尽量避免创建多余的对象。使用“引用”,可以很轻松地实现这一目标。
下面展示了Point类的add函数,另外还有一个操作符函数会调用add函数,这里没有使用引用类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point {
//...
public:
Point add(Point pt);
Point operator+(Point pt);
};

Point Point::add(Point pt) {
Point new_pt;
new_pt.x = x + pt.x;
new_pt.y = y + pt.y;
return new_pt;
}

Point Point::operator+(Point pt) {
return add(pt);
}

这是写这两个函数时最容易想到的方式。但看看pt1+pt2这样的一个表达式会造成多少新对象的创建:

  • 右操作传给operator+函数,所以会创建pt2的一个拷贝,并传给该函数。
  • operator+函数调用add函数,所以需要创建pt2的另一个拷贝,并传给add。
  • add函数新建一个对象,即new_pt。这会调用默认构造函数。函数返回时,程序创建new_pt的一个拷贝,并把它传回调用者(operator+函数) 。
  • operator+函数返回它的调用者,要求再次创建new_pt的一个拷贝。

显然,其中涉及的“拷贝”次数太多了!总共需要创建5个新对象,一次是调用默认构造函数,另外4次都是调用拷贝构造函数。这是一种效率极差的行为。

注意:由于当今的cpu普遍较快,所以你或许并不认为这样做会影响到效率,对于Point这样简单的类,可能需要成千上万(甚至上百万次)重复性的操作,才能感觉到稍微的延迟。但是,你根本不能预料到一个类最终会怎样使用。所以,如果能够简单地改进代码的效率,就值得你去改进!

其中的两个拷贝动作可以利用引用参数来避免。下面是修订后的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point {
//...
public:
Point add(const Point &pt); //改动
Point operator+(const Point &pt); //改动
};

Point Point::add(const Point &pt) {
Point new_pt;
new_pt.x = x + pt.x;
new_pt.y = y + pt.y;
return new_pt;
}

Point Point::operator+(const Point &pt) {
return add(pt);
}

通过使用Point&这样的引用类型,一个好处是虽然函数的实现发生了变化,但不必修改源代码。记住, 当你传递一个引用时,函数获得的是对原始数据的一个引用–只是避免了使用指针语法。
这里还使用了const关键字,它的作用是防止对传递的参数进行修改。函数获得它自己的参数拷贝之后,不管你在函数内部做什么,都无法改动原始拷贝的值。但是,假如传递的是一个引用参数,那么和指针一样,你在函数背部的操作完全有可能造成对原始拷贝的改动。const关键字的作用就是强制进行数据保护,防止函数不慎改变一个参数的值。
进行了上述修订之后,我们避免了两个对象拷贝动作。但是,这两个函数每次返回的时候,都会创建对象的一个新拷贝。为了避免这个拷贝动作,你可以内联其中的一个函数或者两个函数。由于operator+函数的工作十分简单,就是调用一下add函数,所以特别适合内联:

1
2
3
4
5
6
class Point {
//...
public:
Point add(const Point &pt);
Point operator+(const Point &pt) {return add(pt);}
};

像这样内联了operator+函数之后,一旦在程序中遇到pt1+pt2这样的运算,就会将其直接转换成对add函数的调用。


示例1:Point的操作符

现在,你已经掌握了为Point类编写高效、有效的操作符函数所需的一切工具。下面展示了Point类的声明,并在主程序中声明和操作对象,对操作符函数进行测试。
Point3.cpp

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
#include <iostream>
using namespace std;

class Point {
private:
int x, y;
public:
//构造函数
Point() {}
Point(int new_x, int new_y) {set(new_x,new_y);}
Point(const Point &src) {set(src.x,src.y);}

//数学运算
Point add(const Point &pt);
Point sub(const Point &pt);
Point operator+(const Point &pt) {return add(pt);}
Point operator-(const Point &pt) {return sub(pt);}

//其他成员函数
void set(int new_x, int new_y);
int get_x() const {return x;};
int get_y() const {return y;};
};
int main()
{
Point point1(20, 20);
Point point2(0, 5);
Point point3(-10, 25);
Point point4 = point1 + point2 + point3;

cout << "The point is " << point4.get_x()
<< " , " << point4.get_y() << " . " << endl;

return 0;
}
void Point::set(int new_x, int new_y)
{
if(new_x < 0)
new_x *= -1;
if(new_y < 0)
new_y *= -1;
x = new_x;
y = new_y;
}

Point Point::add(const Point &pt)
{
Point new_pt;
new_pt.x = x + pt.x;
new_pt.y = y + pt.y;
return new_pt;
}

Point Point::sub(const Point &pt)
{
Point new_pt;
new_pt.x = x - pt.x;
new_pt.y = y - pt.y;
return new_pt;
}

幕后玄机

本例在Point类中添加了一系列成员函数:

1
2
3
4
Point add(const Point &pt);
Point sub(const Point &pt);
Point operator+(const Point &pt) {return add(pt);}
Point operator-(const Point &pt) {return sub(pt);}

add和sub函数分别对Point对象执行加和减运算,所以你可以像下面写一个语句:
Point point1 = point2.add(point3);
这个语句把point2和point3加到一起,生成一个新的Point对象(point)。operator+是一个内联函数,它能将以下表达式转换成对add函数的调用:
Point point1 = point2 + point3;
由于这个函数是内联的,而且由于它使用的是引用参数(const Point &),所以产生的开销最小。表达式point2+point3转换成对operator+函数的调用,后者再调用add函数。
add函数会新建一个对象(new_pt),它将“调用对象”的坐标和作为参数传递的对象的坐标加到一起。“调用对象”是通过它发出函数调用的那个对象–换言之,在以下表达式中,调用对象的就是point2:
point2.add(point3);
operator-和sub函数以类似的方式工作。
这个例子还在get_x和get_y函数的声明中添加了const关键字。注意,该关键字的位置是在起始大括号({)之前。在这种情况下,const关键字的意思是说:“函数承诺不会更改任何数据成员,也不会调用了除了另一个const函数之外的其他任何函数。”

1
2
Point operator+(const Point &pt) {return add(pt);}
Point operator-(const Point &pt) {return sub(pt);}

之所以要这样写,是考虑到几个原因。第一,它能防止不慎更改数据成员。第二,它允许函数由其他const函数调用。第三,那些承诺不会更改一个Fraction对象的函数(因为它们有一个const Fraction参数的)能够调用该函数。

练习

  1. 写一个测试程序,报告默认函数和拷贝构造函数调用了多少次(提示:插入将输出送到cout的语句,函数定义可以根据需要跨越多行,只要函数定义没有语法上的错误)。运行这个程序,然后将引用参数(const Point &)变回普通参数(Point)。前一种方案在效率上提高了多少。
  2. 编写并测试一个扩展的Point类,它支持一个Point对象和一个整数的乘法运算。请使用全局函数并,并添加friend声明(参见前一章)。

示例2:Fraction类的操作符

本例使用与前一例子享受的技术来扩展对Fraction类的基本操作符支持。和前面一样,我们的代码将使用引用参数(const Fraction &)来保证效率。
Fract6.cpp

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
#include <iostream>
#include <stdlib.h>
using namespace std;
class Fraction {
private:
int num, den; //num分子,den分母
public:
Fraction() {set(0, 1);}
Fraction(int n, int d){set(n, d);}
Fraction(Fraction const &src); //拷贝构造函数

void set(int n,int d) {
num = n;
den = d;
normalize();
}
int get_num() const {return num;}
int get_den() const {return den;}

Fraction add(const Fraction &other);
Fraction mult(const Fraction &other);

Fraction operator+(const Fraction &other) {
return add(other);
}
Fraction operator*(const Fraction &other) {
return mult(other);
}
private:
void normalize() //将分数转换为标准写法
int gcf(int a,int b); //gcf代表最大公因数
int lcm(int a,int b); //lcm代表最小公倍数
};
int main()
{
Fraction f1(1, 2)
Fraction f2(1, 3);

Fraction f3 = f1 + f2;

cout<< "1/2 + 1/3 = ",
<< f3.get_num()<<"/"
<< f3.get_den()<<"."<<endl;
return 0;
}

Fraction::Fraction(Fraction const &src)
{
cout << "Now executing copy constructor."<<endl;
num = src.num;
den = src.den;
}
void Fraction::normalize()
{
//处理0的情况
if( den == 0 || num == 0) {
num = 0;
den = 1;
}
//确保只有分子才能为负数
if(den < 0) {
num *= -1;
den *= -1;
}

//分解出gcf
int n = gcf(num, den);
num = num / n;
den = den / n;
}
//最大公因数
int Fraction::gcf(int a,int b)
{
if(a % b ==0)
return abs(b);
else
return gcf(b,a % b);
}
//最小公倍数
//
int Fraction::lcm(int a, int b) {
return (a / gcf(a, b)) * b;
}
Fraction Fraction::add(const Fraction &other) {
Fraction fract;
int lcd = lcm(den, other.den);
int quot1 = lcd / den;
int quot2 = lcd / other.den;
fract.set(num * quot1 + other.num * quot2, lcd);
return fract;
}
Fraction Fraction::mult(const Fraction &other) {
Fraction fract;
fract.set(num * other.num, den * other.den);
return fract;
}

幕后玄机

add和mult函数沿用自Fraction类的上一个版本。这里惟一改动过的就是参数的类型,确保这两个函数使用的都是引用参数,从而稍微改善一下程序的效率。

1
2
Fraction add(const Fraction &other);
Fraction mult(const Fraction &other);

既然两个函数的声明已经改变,那么函数定义也必须改变,以反映出新的参数类型。但这个改变只影响函数头。其他定义不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
Fraction Fraction::add(const Fraction &other) {  //函数头
Fraction fract;
int lcd = lcm(den, other.den);
int quot1 = lcd / den;
int quot2 = lcd / other.den;
fract.set(num * quot1 + other.num * quot2, lcd);
return fract;
}
Fraction Fraction::mult(const Fraction &other) { //函数头
Fraction fract;
fract.set(num * other.num, den * other.den);
return fract;
}

在任何情况下,Fraction类的操作符函数只是调用恰当的成员函数(add或mult),并返回值。例如,一旦编译器看到以下表达式:
f1 + f2
它就将其转换成以下函数调用:
f1.operator+(f2)
Fraction类的operator+函数就是一个内联函数,它是像下面这样定义的:

1
2
3
Fraction operator+(const Fraction &other) {
return add(other);
}

所以上述函数调用最终会转换成:
f1.add(f2)
乘法运算以类似的方式处理。
Main函数中的语句通过声明分数对象,执行加法运算,并打印结果,从而对操作符函数进行测试:

1
2
3
4
5
6
7
8
Fraction f1(1, 2)
Fraction f2(1, 3);

Fraction f3 = f1 + f2;

cout<< "1/2 + 1/3 = ",
<< f3.get_num()<<"/"
<< f3.get_den()<<"."<<endl;

练习

  1. 修改示例2的main函数,以便提示用户输入一系列分数值,并在用户为一个分母输入0的时候退出输入循环。然后,让程序打印输入的所有分数之和。
  2. 为Fraction类写一个operator-函数(减法运算)。
  3. 为Fraction类写一个operator/函数(除法运算)。

使用其他类型

借助重载,你可以为每个操作符都写出多种不同的函数,让每个函数都支持不同类型的运算。例如,你可以为Fraction类的operator+函数写几个不同的版本:

1
2
3
4
5
6
7
class Fraction {
//...
public:
operator+(const Fraction &other);
friend operator+(int n, const Fraction &fr);
friend operator+(const Fraction &fr, int n);
};

函数的每一个版本都必须在程序的某个地方进行定义。从以上声明可以看出,operator+的几个版本支持int和Fraction类型的操作数的多种组合方式。这样一来,你就可以在程序中写出下面的语句:
Fraction fract1 = 1 + Fraction(1, 2) + Fraction(3, 4) + 4;
但是,还有一种更简单的方法可以支持分数对象与整数运算。你惟一需要的就是一个特殊的“转换函数”,它能将整数转换成Fraction对象。这样一来,就可以只写操作符函数的一个版本。然后,在以下表达式中,编译器会将数字1转换成Fraction格式,再调用Fraction::operator+函数来执行两个分数对象的加法运算:
Fraction fract1 = 1 + Fraction(1, 2);
事实上,用于支持上述写法的“转换函数”很容易写,你只需提供一个Fraction构造函数,让它只获取一个int类型的值作为参数。由于它非常简单,所以适合写成内联函数来提高效率:
Fraction(int n) {set(n, 1);}
有了上述声明之后,为两个Fraction对象声明的所有数学运算都会自动扩展到Fraction对象和整数之间的运算,你不需要再去写操作符函数的重载版本。


类赋值函数(=)

写一个类时,c++编译器会友好地自动提供3个特殊的成员函数。我们之前已经介绍了其中的两种:

  • 默认构造函数。编译器自动提供的版本会将每个成员初始化为0。但是,只要你自己写了任何一个构造函数,编译器都不会再提供默认构造函数。所以,为了保险起见,最好养成总是自己写一个默认构造函数的习惯,即使它什么事情都不做。
  • 拷贝构造函数。编译器自动提供的版本会对原始对象执行逐个成员的拷贝。
  • 赋值操作符函数(operator=)。这是以前没有讲过的。

赋值操作函数的特殊性在于,假如你自己没有写,编译器就会自动提供一个。正是因为这个原因,我们在以前的程序中才能直接执行像下面的运算:

1
2
Fraction f1;
f1 = f2 + f3;

operator=函数的默认行为和拷贝构造函数相似:执行一次简单的、对每个成员的拷贝。因此,你当然会怀疑:赋值操作函数和拷贝构造函数是一码事码?
答案是否定的,虽然两者表面上的效果相同。在两种情况下,都是将一个对象中的所有值(默认情况下)拷贝到另一个对象。区别在于,拷贝构造函数能自动初始化一个新对象,而赋值操作函数只能将值拷贝到一个现有的函数。某些情况下(比如在类需要申请内存空间,或者打开一个文件的时候),拷贝构造函数要做的工作比赋值操作符函数更多。
写自己的赋值操作符函数时,请使用以下语法:
class_name &operator=(const class_name &source_arg)
注意,虽然上述声明类似于拷贝构造函数,但operator=函数必须返回一个对象的引用,同时还要获取一个引用参数。
下面列出了Fraction类的operator=函数:

1
2
3
4
5
6
7
8
class Fraction {
//...
public:
Fraction &operator=(const Fraction &src) {
set(src.num, src.den);
return *this;
}
};

上述代码使用了一个新的关键字,即this。我们准备在下一章解释this关键字的用法,以及operator=函数的其他特点。
你现在只需知道一点:对于这样的类,没有必要专门写一个赋值操作函数。相反,完全可以利用它的默认行为,而且假如你没有提供赋值操作符函数,编译器肯定会自动提供一个。


测试相等性函数(==)

测试相等性操作符则不同。编译器不会为你的类自动提供一个operator==函数。所以,假如你没有专门写一个operator==函数,以下代码是无法工作的:

1
2
3
4
5
6
7
Fraction f1(2, 3);
Fraction f2(4, 6);

if( f1 == f2)
cout << "The fractions are equal.";
else
cout << "The fractions are not equal."

显然,上述代码应该打印两个分数相等的消息,即使两个对象包含的不同的数字(2/3和4/6)。
那么可不可以简单地比较一下两个对象的分子和分母(num和den),就能做出正确的判断呢?答案是肯定的。这是由该类的构造方式决定的。我们在写这个类时,确保了只要为分数设置了值,它就会立即进行简化,变成一个惟一的数学表达式(例如,-10 / -20会简化成1/2)。
所以,很容易就可以写出一个测试相等性操作符函数(operator==)。如果分子和分母都相等,那么两个分数肯定是相等的。如下所示:

1
2
3
4
5
6
int Fraction::operat(const Fraction &other) {
if( num == other.num && den == other.den)
return true;
else
return false;
}

上述函数定义还可以简化,具体做法是直接返回条件表达式的结果:

1
2
3
int Fraction::operator==(const Fraction &other) {
return (num == other.num && den == other.den);
}

函数定义非常简单,所以很适合作为一个内联函数:

1
2
3
4
5
6
7
class Fraction {
//...
public:
int Fraction::operator==(const Fraction &other) {
return (num == other.num && den == other.den);
}
};

引用参数类型(const Fraction &)能够进一步增强程序的效率,所以这里一并使用。

小插曲:可不可以使用布尔类型(bool)?

c++的最新版本支持一种特殊的布尔类型,即bool。该类型等价于int类型,但两者存在一个重要区别:虽然可以将自己喜欢的任何值指定为bool值,但任何非负的bool值都会自动转换成true(1)。
一个良好的编程习惯是在你的编译器支持的前提下,尽可能地使用bool类型。由于bool值专门用于容纳一个true值或者false值,所以该类型能够显著增强代码的可读性,让人立即明白代码的作用。
函数的bool版本只需修改一下返回类型即可。其他所有内容都无需更改。

1
2
3
4
public:
bool operator==(const Fraction &other) {
return (num == other.num && den == other.den);
}

类的打印函数

在我们获得Fraction类的一个基本完整的版本之前,还有一件事情必须做。你可能已经注意到,每次需要打印一个分数的内容时,都要写以下代码:

1
2
cout << f3.get_num()<<"/"
<< f3.get_den()<<"."<<endl;

显然,经常使用类似的语句是一件很麻烦的事,为了增强这个类的“可用性”,必须避免这种繁琐的写法。
为此,最容易想到的一个方案是专门写一个函数。由于每个类都有它自己的数据格式,所以每个类都应该有它自己的“打印”函数。你甚至可以直接将一个成员函数取名为“print”,因为它不是C++的保留字:

1
2
3
4
void Fraction::print() {
cout << "num" << "/"
<< "den" <<endl;
}

但是,虽然这是一种有效的解决方案,但还不是最好的。一种更面向对象的方案是利用“cout本身就是对象”这一事实。理想的“打印”函数应该和cout(以及其他ostream对象,比如输出文件)进行交互。
最好的方案应该是能够像处理其他基本数据类型那样处理Fraction。换言之。你应该能使用以下语句:

1
cout << fract;

为了支持这种写法,一个方案是写一个operator<<函数,让它和cout的父类(ostream)进行交互。函数必须是一个全局函数,因为左操作数是ostream类的一个对象,而我们没有更新或修改ostream代码的权限。
函数应该声明为Fraction类的一个友元,这样才能访问私有成员:

1
2
3
4
5
class Fraction {
//...
public:
friend ostream &operator<<(ostrean &os, Fraction &fr);
};

注意,函数返回的是对一个ostream对象的引用。只有这样,以下语句才能成功执行:

1
cout << "The value of the fraction is " < fract<<endl;

下面是operator函数的定义:

1
2
3
4
ostream &operator<<(ostream &os, Fraction &fr) {
os << fr.num << "/" << fr.den;
return os;
}

这个方案的好处在于,他能将Fraction的显示正确定向到你指定的任何ostream对象,其中包括文件流对象以及控制台输出对象cout。例如,假定outfile是一个文本文件输出对象,你就可以利用它将一个分数打印到文件中:

1
outfile << fact;

示例3:完整的Fraction类

虽然你还可以为Fraction类添加更多扩展(尤其是对减法和除法运算的支持,不过这已经前面的一道练习题中完成了),但这个类目前已经是相当完整了。本例将展示这个类的最终形式,它具有足够强大的功能,相当有用。
另外要注意的是,你不需要在使用该类的每个程序都包含类的完整代码。相反,你可以将独立的类函数(也就是没有内联的函数)放到单独的模块中,它们只需编译一次,生成的目标文件(.o文件)可以链接到需要用它们的任何项目中。另外,你还需要将类的声明放到它自己的头文件中(Fract.h)。然后。在需要使用该类的任何程序开头,请添加如下语句:
#include "Fract.h"
下面展示了Fraction类的完整版本,以及对它进行测试的一些代码。

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
#include <iostream>
#include <stdlib.h>
using namespace std;
class Fraction {
private:
int num, den; //num分子,den分母
public:
Fraction() {set(0, 1);}
Fraction(int n, int d){set(n, d);}
Fraction(int n) {set(n, 1);} //新增
Fraction(Fraction const &src); //拷贝构造函数

void set(int n,int d) {
num = n;
den = d;
normalize();
}
int get_num() const {return num;}
int get_den() const {return den;}

Fraction add(const Fraction &other);
Fraction mult(const Fraction &other);

Fraction operator+(const Fraction &other) {
return add(other);
}
Fraction operator*(const Fraction &other) {
return mult(other);
}
int operator==(const Fraction &other); //新增
friend ostream &operator<<(ostream &os, Fraction &fr); //新增
private:
void normalize() //将分数转换为标准写法
int gcf(int a,int b); //gcf代表最大公因数
int lcm(int a,int b); //lcm代表最小公倍数
};
int main()
{
Fraction f1(1, 2)
Fraction f2(1, 3);

Fraction f3 = f1 + f2 + 1;

cout<< "1/2 + 1/3 + 1 = " << f3 << endl;
return 0;
}

Fraction::Fraction(Fraction const &src)
{
cout << "Now executing copy constructor."<<endl;
num = src.num;
den = src.den;
}
void Fraction::normalize()
{
//处理0的情况
if( den == 0 || num == 0) {
num = 0;
den = 1;
}
//确保只有分子才能为负数
if(den < 0) {
num *= -1;
den *= -1;
}

//分解出gcf
int n = gcf(num, den);
num = num / n;
den = den / n;
}
//最大公因数
int Fraction::gcf(int a,int b)
{
if(a % b ==0)
return abs(b);
else
return gcf(b,a % b);
}
//最小公倍数
//
int Fraction::lcm(int a, int b) {
return (a / gcf(a, b)) * b;
}
Fraction Fraction::add(const Fraction &other) {
Fraction fract;
int lcd = lcm(den, other.den);
int quot1 = lcd / den;
int quot2 = lcd / other.den;
fract.set(num * quot1 + other.num * quot2, lcd);
return fract;
}
Fraction Fraction::mult(const Fraction &other) {
Fraction fract;
fract.set(num * other.num, den * other.den);
return fract;
}

int Fraction::operator==(const Fraction &other) {
return (num == other.num && den == other.den);
}

ostream &operator<<(ostream &os, Fraction &fr) {
os << fr.num << "/" <<fr.den;
return os;
}

幕后玄机

本例为Fraction类添加了以下函数:

  • 一个新的构造函数,它只获取一个参数。
  • 一个操作符函数,它支持“测试相等性”操作符(==)。
  • 一个全局函数,它允许将Fraction对象直接打印到一个ostream对象中,比如cout

新的构造函数非常简单,完全应该内联(将定义直接嵌入类的声明中)。如前所述,这种构造函数的一个好处在于,它定义了如何将一个整数值转换成一个Fraction对象,比如1会转换成1/1。这样一来,你就没有必要添加操作符函数的许多重载版本来支持分数和整数混合的数学运算。相反,程序会先将整数转换成Fraction对象,然后再执行纯分数的运算:
Fraction(int n) {set(n, 1);}
该函数将作为参数传递的整数设为分子,将1设为分母。换言之,1会转换成1/1,2会转换成2/1,5会转换成5/1,以此类推。
这个转换符合我们设定好的数学运算前提。整数5转换成5/1之后,它的值实际没有发生变化,但是变成了Fraction格式,这正是我们执行分数计算的前提。
对Fraction类的其他扩展成了前一节所讨论的代码。首先,类的声明进行了扩展,它声明了两个新函数:

1
2
int operator==(const Fraction &other);
friend ostream &operator<<(ostream &os, Fraction &fr);

第一个函数是operator==,它是Fraction类的一个真正的成员函数,它在类的外部要标识为Fraction::operator==,以澄清它的作用域。由于它是一个成员函数,所以只能通过一个特定的对象来调用:

1
2
3
int Fraction::operator==(const Fraction &other) {
return (num == other.num && den == other.den);
}

记住,如果不加限定地使用 num 和 den,这里值得就是“调用对象”(即左操作数)的成员。相反,表达式other.num和other.den是指右操作数的值。
operator<<函数的声明表明它是一个全局函数,但同时是对Fraction类的一个友元。所以,它能访问类的私有数据(num个den)。这是一个重载的函数。编译器能根据参数列表的类型来区分不同的版本。下面再次列出了它的定义:

1
2
3
4
ostream &operator<<(ostream &os, Fraction &fr) {
os << fr.num << "/" << fr.den;
return os;
}

练习

  1. 修改示例3的operator<<函数,让它以“(n,d)”的格式来打印分数。其中,n是分子,d是分母。
  2. 写一个大于(>)和小于(<)函数,并修订示例3的main函数来测试它们。例如,你的程序应该测试出1/2+1/3大于5/9。提示:如果AD>BC,那么A/B大于C/D。
  3. 写一个operator<<函数,将一个Point对象的内容发生给一个ostream对象(比如cout)。假定函数声明为Point类的一个友元函数。请写出相应的函数定义。

小结

  • 一个类的操作符函数应该具有以下声明,其中的@代表任何有效的c++操作符。return_type operator@(argument_list)
  • 操作符函数即可以声明为一个成员函数,也可以声明为一个全局函数。如果它是成员函数,那(对于二元操作符来说)就应该有一个参数。例如,Point类的operator+函数应该具有以下声明和定义:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Point {
    // ...
    public:
    Point operator+(Point pt);
    };

    Point Point::operator+(Point pt) {
    Point new_pt;
    new_pt.x = x + pt.x;
    new_pt.y = y + pt.y;
    return new_pt;
    }
  • 基于上述代码,编译器知道了如何解释类的两个对象之间出现的加号:
    point1 + point2
  • 如果像这样使用操作符函数,左操作数就称为“调用对象”。换言之,函数将通过该对象来调用。相反,右操作数将成为函数的参数。所以,在上面的operator+函数中,未限定的x和y引用是指左操作数的值(成员)。
  • 也可以将操作符函数声明为全局函数。对于二元操作符来说,函数应该有两个参数。例如:
    1
    2
    3
    4
    5
    6
    Point operator+(Point pt1, Point pt2) {
    Point new_pt;
    new_pt.x = pt1.x + pt2.x;
    new_pt.y = pt1.y + pt2.y;
    return new_pt;
    }
  • 像这样写操作符函数,一个缺点在于它失去了访问私有成员的权限。为了解决这个问题,可以将全局函数声明为类的友元。例如:
    1
    2
    3
    4
    5
    class Point {
    //...
    public:
    friend Point operator+(Point pt1, Point pt2);
    };
  • 假如一个参数需要获取一个对象,但是不需要修改它的内容,那么你总是可以利用一个引用参数来增强函数的执行效率。例如,可以将上例的Point类型变成const Point &类型。
  • 可以使用只有一个参数的构造函数来提供转换功能。例如,以下构造函数能将整数数据自动转换成Fraction对象格式:
    Fraction(int n) {set(n, 1);}
  • 如果你不写一个赋值操作符函数(operator=),编译器会自动提供一个。对于这个自动提供的版本,它的作用是执行简单的逐成员的拷贝。
  • 编译器不会自动提供一个“测试相等性”函数(operator==)。如果你想比较两个对象的大小,就必须自己写一个。如果编译器支持,那么最好为这个函数使用bool返回类型;否则,就使用int返回类型。
  • 为类写一个“打印”函数的最佳做法是写一个operator<<函数的重载版本,它应该是一个全局函数,但作为类的友元,能够访问类的私有数据。第一个参数应该具有ostream类型,使流操作符(<<)能够支持cout以及ostream作为基类的其他所有类。首先要将该函数声明为你的类的一个友元:
    1
    2
    3
    4
    5
    class Point {
    //...
    public:
    friend ostream &operator<<(ostream &os, Fraction &fr);
    };
  • 在函数定义中,语句应该来自右操作数(本例就是fr)的数据写入由ostream参数指定的对象。然后,函数应该返回这个ostream对象。例如:
    1
    2
    3
    4
    ostream &operator<<(ostream &os, Fraction &fr) {
    os << fr.num << "/" << fr.den;
    return os;
    }

c++学习记录三:操作符函数

http://example.com/2018/01/23/cplusplus3/

作者

bd160jbgm

发布于

2018-01-23

更新于

2021-05-08

许可协议