必备技能9.11:以非成员函数的方式重载运算符函数
除了前面讨论的采用类的成员函数的方式重载运算符外,我们还可以以非成员函数的方式来重载运算符,那就是通过类的友元函数。我们在前面已经讨论到友元函数没有this指针。因此,当使用友元函数的方式来重载运算符的时候,对于双目运算符来说,两个运算数都是要显示地通过参数的方式传递给运算符函数的;对于单目运算符来说,那一个参数也是要显示地通过参数的方式传递给运算符函数的。但是有以下的几个运算符是我们不能通过友元函数的方式来进行重载的:=,(),[]和->。
下面的程序展示了如果使用友元函数的方式来实现对ThreeD类的+运算符的重载:
//使用友元函数实现运算符的重载
#include <iostream>
using namespace std;
class ThreeD
{
int x, y, z;
public:
ThreeD()
{
x = y = z;
}
ThreeD (int i, int j , int k)
{
x = i;
y = j;
z = k;
}
friend ThreeD operator+(ThreeD op1, ThreeD op2);
void show();
};
// operator+()是友元函数
ThreeD operator+(ThreeD op1, ThreeD op2)
{
ThreeD temp;
temp.x = op1.x + op2.x;
temp.y = op1.y + op2.y;
temp.z = op1.z + op2.z;
return temp;
}
// 显示 x, y, z坐标
void ThreeD::show()
{
cout << x << ",";
cout << y << ",";
cout << z << "\n";
}
int main()
{
ThreeD a(1,2,3), b(10,10,10), c;
cout << "Original valud of a: ";
a.show();
cout << "Original value of b: ";
b.show();
cout <<"\n";
c = a + b; // 把a和b相加
cout << "Value of c after c = a +b : ";
c.show();
cout << "\n";
c = a + b + c; // 把a,b,c相加
cout << "Value of c after c = a + b + c: ";
c.show();
cout << "\n";
c = b = a; // 多重赋值
cout << "Value of c after c = b = a : ";
c.show();
cout << "Value of b after c = b = a: ";
b.show();
return 0;
}
上面程序的输出为:
Original valud of a: 1,2,3
Original value of b: 10,10,10
Value of c after c = a +b : 11,12,13
Value of c after c = a + b + c: 22,24,26
Value of c after c = b = a : 1,2,3
Value of b after c = b = a: 1,2,3
从上面的程序中可以看出,重载的+运算符的两个运算数都是通过参数传递的方式显示地传入到operator+()函数中的。左侧的参数传递给参数op1,右侧的参数传递给了参数op2。
在许多情况下,使用友元函数的方式来重载运算符和使用类的成员函数的方式相比并不能带来明显的好处。但是,有一种情形使用友元函数会非常有效:当我们希望内置类型对象出现在双目运算符的左侧的时候。这是为什么呢?我们都知道,调用成员函数的对象是通过this指针传入到函数中的。针对双目运算符的情况,这个对象就是运算符左侧的运算数。所以只要运算符左侧的运算数明确定义了这种运算符的含义就是可以的。例如,有对象T,假定我们已经为其定义了赋值运算符和+运算符的操作,那么下面的语句就是有效的:
T = T + 10; //有效的语句
这是因为对象T是出现在运算符的左侧的,由它来调用+运算符函数来完成加法的运算。然而,下面的语句则是无效的:
T = 10 + T; //无效的语句
上面这个语句的问题就在于出现在+运算符左侧的是一个整形数,一种内置的数据类型,其没有明确定义如何和对象T的类型进行加法运算。解决这个问题的方法就是使用两个友元函数来重载+运算。这样以来,重载的两个函数会显式地传入两个运算数,可以像其它重载的函数那样基于参数的类型来被调用。这两个友元函数中的一个用来处理T类型的对象和整数相加的操作,另外一个用来处理整数和T类型的对象的加法。这样以来,通过使用友元函数的方式就使得内置类型的数据也可以出现在运算符的左侧及右侧。下面的程序就演示了这种技术。其中定义了针对ThreeD类定义了两版本的+运算。它们都是完成整数和ThreeD对象相加的功能。其中整数是可以出现在运算符的左侧及右侧的。
//重载加号运算,实现 整数+对象 和 对象+ 整数
#include <iostream>
using namespace std;
class ThreeD
{
int x, y ,z; //三维坐标
public:
ThreeD()
{
x = y = z = 0;
}
ThreeD ( int i, int j , int k )
{
x = i;
y = j;
z = k;
}
friend ThreeD operator+(ThreeD op1, int op2); // 对象 + 整型数
friend ThreeD operator+(int op1, ThreeD op2); // 整型数 + 对象
void show();
};
//实现 ThreeD + 整型数
ThreeD operator+(ThreeD op1, int op2)
{
ThreeD temp;
temp.x = op1.x + op2;
temp.y = op1.y + op2;
temp.z = op1.z + op2;
return temp;
}
//实现 整型数 + ThreeD
ThreeD operator+(int op1, ThreeD op2)
{
ThreeD temp;
temp.x = op1 + op2.x;
temp.y = op1 + op2.y;
temp.z = op1 + op2.z;
return temp;
}
//显示 x, y, z 坐标
void ThreeD::show()
{
cout << x << ", ";
cout << y << ", ";
cout << z << "\n ";
}
int main()
{
ThreeD a(1,2,3), b;
cout << "Origianl value of a: ";
a.show();
cout << "\n";
b = a + 10; //对象 + 整型数
cout << "Value of b after b = a + 10 : ";
b.show();
cout << "\n";
b = 10 + a; // 整型数 + 对象
cout << "Value of b after b = 10 + a : ";
b.show();
return 0;
}
上面程序的输出如下:
Origianl value of a: 1, 2, 3
Value of b after b = a + 10 : 11, 12, 13
Value of b after b = 10 + a : 11, 12, 13
由于在上面的程序中个,我们对+()进行了两次重载,这样就可以处理整型数和ThreeD对象相加的两种情况了。
使用友元函数来重载单目运算符
我们还可以使用友元函数来实现对单目运算符的重载。然而,如果我们使用友元函数来重载++或者--运算符,我们必须把运算数以引用的方式传递到对应的运算符函数中。由于引用是一种隐式的指针,那么在函数中对形参的修改都会影响到传入的实参。因此采用传递引用的方式可以实现对象的自增或者自减运算。当我们使用友元函数来重载自增运算符的时候,其前缀形式需要一个参数,也就是运算数;而后缀形式则需要两个参数。其中第二个参数是一个整型数,但是这个参数不会被使用。下面的代码段就展示了如何使用友元函数来对前缀和后缀的自增运算进行重载:
//采用友元函数的方式重载前缀自增运算符,其中使用到传递引用*/
ThreeD operator++(ThreeD &op1)
{
op1.x++;
op1.y++;
op1.z++;
return op1;
}
//采用友元函数的方式重载后缀自增运算符,其中使用到传递引用
ThreeD operator++(ThreeD &op1, int notused)
{
ThreeD temp = op1;
op1.x++;
op1.y++;
op1.z++;
return temp;
}
练习:
1. 以非成员函数的方式重载双目运算符函数,需要几个参数?
2. 当使用非成员函数的方式重载++运算符的时候,应该以什么样的方式进行参数传递?
3. 使用友元函数的方式重载运算符函数的一个好处就是可以让内置类型的数据出现在运算符的左侧,对吗?
关于运算符重载的小提示和一些限制
针对类定义的重载运算符可以和该运算符作用于内置数据类型含义没有什么关系、例如,引用于cout和cin的<<和>>运算符就和该运算符作用于整型数的含义没有什么关系。然而,为了程序的结构化和代码的可阅读性,重载的运算符应该尽可能地反映出该操作的原本含义。例如,ThreeD类的+运算符就和整型数的+运算符在概念上相近。如果我们针对别的类定义+运算来完成||运算符的功能也是可以的。只是这样作并没有什么好处。这里想要说明的就是尽管我们可以在重载运算符的时候随意地定义其功能,但是出于代码清晰度的考虑,我们最好还是在定义它的新功能时,保持其和原始功能相关。
专家答疑
问:当对关系运算符进行重载的时候,有什么需要特别注意的地方吗?
答:重载诸如==或者<之类的关系运算符的方法和我们学习的方法一样简单的。但是,有一个小问题需要注意。众所周知,重载的运算符函数通常都是返回相关类的对象。然而,重载的关系运算符函数却应该返回true或者false。这点是和普通的关系运算符保持一致的。这样可以使得重载的关系运算符函数也可以在条件表达式中使用。这点对于逻辑运算符的重载也是一样的。
下面的程序演示了如果针对ThreeD类来重载==运算符。通过这个例子展示关系运算符重载的实现。
bool ThreeD::operator ==(ThreeD op2)
{
if ( (x == op2.x )&&(y == op2.y )&&( z == op2.z))
{
return true;
}
else
{
return false;
}
}
经过上面的对==运算符的重载后,下面的代码就是完全有效的:
ThreeD a(1,2,3), b(2,2,2) ;
if ( a == b)
cout << " a equals b \n";
else
cout << "a does not equal b \n";
运算符的重载是有一些限制的。第一,不能修改运算符的优先级。第二,尽管运算数可以被忽略,但是运算数的数量也是不能修改的。最后,除了函数调用运算符外,运算符函数不支持缺省参数。
几乎所有的C++运算符都是可以被重载的。这也包括特殊的运算符,例如数组索引运算符[],函数调用运算符()以及->运算符。但是下面的运算符是不能被重载的:
. :: .* ?
其中上面的.*是一种很特殊的运算符,它的使用方法不在本书中进行 讨论。