0 前言
为什么要重载运算操作符?
因为我们希望能够很方便地应用运算符的大部分概念都不是c++的内部类型,这些概念必须用用户定义类型表示。例如c++中的复数算术,矩阵代数等。你要为这些类定义各种运算符,使之的记法形式和计算方式更符合人们的习惯。
那么我们先来看看哪些运算符不能重载。以下运算符不能由用户定义:
:: 作用域解析符
. 成员选择符
.* 通过成员指针做成员选择符
不能重载的原因是这些运算符都以名字而不是数值作为其第二个参数,而且提供的是引用成员的基本语义。还有三目运算符?:,sizeof,typeid都不能重载的。剩下的基本上都可重载。
还有一点需要强调的是必要定义新的运算符单词,如果你觉得运算符不够用,可以用函数嘛。
那么,下面开整。
1 成员运算符重载与非成员运算符重载
直接看例子:
class X{
public:
X operator +(X &a);
};
成员运算符重载的第一个参数已经提供,即默认为该类对象。后面括号跟着的参数为运算符的第二个参数。显然此成员运算符的调用方式可以与调用成员函数类似,可以:
X a,b;
a.operator+(b); 或者直接a+b
显然第一个参数必须是该类对象的时候才能调用该类的成员运算符。也就是该类的成员函数必须由第一个对象去激活。
那么我们再来看看非成员运算符。
X operator+ (X &a,X &b);
a为双目运算符的左参数,b为其右参数。根据函数重载规则,必须参数匹配才可正确调用。
当然,有时候也会涉及一些参数类型转换,这里先不细述。
接下来我们来看看友元运算符重载函数,为什么要声明为友元函数呢?
当你希望一个普通函数能够能够具有成员函数的功能--直接访问类声明的私用部分时候就将这个函数声明为友元函数。下面来看一个例子:
class Matrix{
friend Matrix operator *(const Matrix &,const Matrix &);
//.........
};
需要说明的是friend声明可以放在类中的私用或者公用部分,放在哪里都没有关系。一个友元函数或者需要再某个外围作用域里显式声明,或者以它的类或由该类派生的类作为一个参数,否则无法调用这个友元函数。
那么晚我们会遇到一个问题,什么时候用友元函数什么时候用成员函数去刻画一个运算符才是更好的选择呢?
来看一个例子:
class X{
public:
X(int);
int M1();
friend int f1(X&);
friend int f2(const X&);
};
void g(){
9.m1(); //错误,不会将9转换为X(9).m1()
f1(9); //错误,不会隐式转换为f1(X(9))
f2(9); //ok f2(X(9))
}
上文已提到类型转换,那么下面就来说说类型转换。
2类型转换
我们来看一个例子:
complex d = 2 + b;
complex d = b + 2;
complex d = a + b;
//针对上述类型,我们是否需要定义3个版本的运算符重载函数?
complex operator + (complex,complex);
complex operator + (complex,double);
complex operator + (double,complex);
这是很令人厌烦的事情,而厌烦是容易出错的根源。与因为参数的不同组合分别提供函数版本相对应的还有另外一个方式,那就是依赖类型转换。例如:如果我们的complex类提供了这样一个构造函数,它将double转换到complex,由于这种情况我们只需要为complex的”+“运算定义一个版本(声明为friend类型)。
Complex operator + (Complex,Complex);
void f(Complex x,Complex y){
x + y ; //ok
3.operator+(y); //错误,3不是类对象
x + 3; //operator(x,Complex(3))
3 + y; //operator(Complex(3),y)
}
然而事情也不总是那么绝对,在一些情况下,转换将带来大量额外开销,我们可能更倾向于定义多个不同的版本。
通过构造函数去刻画类型转换确实很方便,但也会发生一些我们并不希望的情况。构造函数不能刻画:① 从用户定义类型到内部类型的转换(因为内部类型不是类)。② 从新类型到某个已有类型的转换(而不去修改那个已有类的声明)。上述问题可以通过一为源类型定义转换运算符的方式解决。成员函数X::operator T(),就定义了一个从X到T的转换。这里T最好是内部类型否则还要做一些其他的事情,例如:
class Ting{
int v;
public:
operator int()const {
return v; //转换到int函数
}
};
请注意上述函数,我们并没有写出返回值类型。因为编译器知道这是一个类型转换函数。转换目标的类型已作为运算符名字的一部分,不能作为转换函数逇返回值类型重复写出。
既然,有了隐式类型转换,那么就会存在另外一个问题----歧义性。我们来看下面的例子:
class X{/*........*/X(int);X(char *);};
class Y{/*.....*/ Y(int);};
X f(X);
Y f(Y);
void k1(){
f(1); // 错误:歧义的f(X(1))或f(Y(1))
f(X(1)); // ok
f(Y(1)); //ok
}
所以在具体应用的时候还是要注意这方面的问题。