C++用顶层函数重载操作符

前面章节中,我们已经学会了将操作符重载函数声明为类的成员函数。除此之外,还可以将操作符重载函数声明为顶层函数。

学习将操作符重载函数声明为类成员函数时,我们不断强调二元操作符的函数参数为一个,一元操作符重载函数不需要函数参数。如果以顶层函数的形式重载操作符时,二元操作符重载函数必须有两个参数,一元操作符重载必须有一个参数。

将操作符重载函数声明为顶层函数时,必须至少有一个类对象参数,否则编译器无法区分操作符是系统内建的还是程序设计人员自己定义的,有了一个类对象参数之后,系统则会根据情况调用内建或自定的操作符。

【例 1】
#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    double getreal() const { return real; }
    double getimag() const { return imag; }
    void setreal(double a){ real = a; }
    void setimag(double b){ imag = b; }
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex operator+(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() + B.getreal());
    C.setimag(A.getimag() + B.getimag());
    return C;
}

//重载减法操作符
complex operator-(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() - B.getreal());
    C.setimag(A.getimag() - B.getimag());
    return C;
}

//重载乘法操作符
complex operator*(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() * B.getreal() - A.getimag() * B.getimag() );
    C.setimag(A.getimag() * B.getreal() + A.getreal() * B.getimag() );
    return C;
}

//重载除法操作符
complex operator/(const complex & A, const complex & B)
{
    complex C;
    double square = A.getreal() * A.getreal() + A.getimag() * A.getimag();
    C.setreal((A.getreal() * B.getreal() + A.getimag() * B.getimag())/square);
    C.setimag((A.getimag() * B.getreal() - A.getreal() * B.getimag())/square);
    return C;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;
   
    c3 = c1 + c2;
    cout<<"c1 + c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 - c2;
    cout<<"c1 - c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 * c2;
    cout<<"c1 * c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 / c2;
    cout<<"c1 / c2 = ";
    c3.display();
    cout<<endl;

    return 0;
}
本例中,给大家演示了以顶层函数的方式重载加法、减法、乘法和除法操作符,使之分别具有对 complex 类对象的加减乘除功能。因为是以顶层函数的形式重载操作符的,因此类中没有声明操作符重载函数。为了能够在类外操作 real 和 imag 两个数据成员,我们为类添加了 getimag()、getreal()、setimag() 和 setreal() 函数。

我们以加法操作符为例,来看看如何以顶层函数的方式重载它。加法操作符重载函数的函数头complex operator+(const complex & A, const complex &B),因为加法操作符重载后可以计算复数的加法,返回的仍然是一个复数,函数的返回值仍然是 complex。操作符重载函数参数为两个 complex 类对象的引用,加法操作符为二元操作符,因此必须要有两个操作数,函数有两个参数。函数体部分比较简单,只是使用 getimag()、getreal()、setimag() 和 setreal() 四个函数来实现复数的加法操作而已。

其它普通操作符的重载与例 1 中的加法操作符重载类似。如果我们重载的是一元操作符,则函数需要有一个参数。

【例 2】
class test
{
    //......
};

test operator!(test & A)
{
    //......
}
本例中以顶层函数的形式重载非操作符符,因为其为一元操作符,故而函数有一个参数。

以顶层函数的形式重载操作符,其调用方法与普通函数调用类似。例如本节例 1 中的复数类,我们调用加法重载函数时可以采用如下方法:
complex c1, c2, c3;
c3 = operator+( c1, c2);
这样的函数调用方法和普通的函数调用方法一样,但是由于 operator 关键字的作用,我们还可以采用另外一种熟知的调用方法:
c3 = c1 + c2;
这种调用方法和先前以类成员函数的形式重载操作符调用方法一样。本节例 1 中也都是采用这种简单明了的调用方法。

需要注意的是,指针操作符“->”、下标操作符“[]”、函数调用操作符“()”和赋值操作符“=”只能以成员函数的形式进行操作符重载。

从函数实现上来看,以顶层函数的形式重载操作符相对于以类成员函数的形式实现要复杂一些,因为在类外无法直接访问类的私有成员变量。但是以顶层函数的形式来重载操作符有自身的优势,我们来看下面的示例。

【例 3】
#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    complex operator+(const complex & A)const;
    complex operator-(const complex & A)const;
    complex operator*(const complex & A)const;
    complex operator/(const complex & A)const;
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex complex::operator+(const complex & A)const
{
    complex B;
    B.real = real + A.real;
    B.imag = imag + A.imag;
    return B;
}

//重载减法操作符
complex complex::operator-(const complex & A)const
{
    complex B;
    B.real = real - A.real;
    B.imag = imag - A.imag;
    return B;
}

//重载乘法操作符
complex complex::operator*(const complex & A)const
{
    complex B;
    B.real = real * A.real - imag * A.imag;
    B.imag = imag * A.real + real * A.imag;
    return B;
}

//重载除法操作符
complex complex::operator/(const complex & A)const
{
    complex B;
    double square = A.real * A.real + A.imag * A.imag;
    B.real = (real * A.real + imag * A.imag)/square;
    B.imag = (imag * A.real - real * A.imag)/square;
    return B;
}

int main()
{
    complex c1, c2(15.5, 23.1);
    c1 = c2 + 13.5;
    c1 = 13.5 + c2;
    return 0;
}
本例中是以成员函数的形式进行操作符重载的,在主函数中我们定义了两个 complex 复数类的对象,语句“c1 = c2 + 13.5;”是将 c2 与一个 double 类型的数据相加,我们可以将其理解为:
c1 = c2.operator+(13.5);
因为我们在类中定义了只带一个参数的构造函数complex(double a);,这个构造函数可以视为转型构造函数,它可以将 double 类型转换为一个 complex 类对象。因此 c1 = c2 + 13.5;语句其实也是相当于两个复数类对象相加。当然,如果在类中没有定义complex(double a);,那么这一句也是有语法问题的,因为我们重载的加法只适用于两个 complex 类对象相加,而系统内建的又只能用于两个普通数据类型的操作数相加,一个 complex 类对象和一个普通数据类型的操作数相加,系统无法处理这样的异常情况。

我们再来看一下后面一个语句c1 = 13.5 + c2;,这一语句我们可以将其理解为:
c1 = 13.5.operator+(c2);
该语句的问题非常明显,13.5 只是一个 double 类型的常数,它不是类对象,因此也不可能有调用 operator+() 的能力。虽然我们在类中定义了一个具有一个参数的构造函数,但是编译器只会将语句c1 = 13.5 + c2;理解成c1 = 13.5.operator+(c2);,而不会将 13.5 转换成一个 complex 类对象,因为编译器遇到这种情况并不会产生一种很智能的处理,同样它也并不知道程序设计人员的意图。所以例 3 中语句c1 = 13.5 + c2;是有语法错误的。

【例 4】
#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    double getreal() const { return real; }
    double getimag() const { return imag; }
    void setreal(double a){ real = a; }
    void setimag(double b){ imag = b; }
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex operator+(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() + B.getreal());
    C.setimag(A.getimag() + B.getimag());
    return C;
}

//重载减法操作符
complex operator-(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() - B.getreal());
    C.setimag(A.getimag() - B.getimag());
    return C;
}

//重载乘法操作符
complex operator*(const complex & A, const complex &B)
{
    complex C;
    C.setreal(A.getreal() * B.getreal() - A.getimag() * B.getimag() );
    C.setimag(A.getimag() * B.getreal() + A.getreal() * B.getimag() );
    return C;
}

//重载除法操作符
complex operator/(const complex & A, const complex & B)
{
    complex C;
    double square = A.getreal() * A.getreal() + A.getimag() * A.getimag();
    C.setreal((A.getreal() * B.getreal() + A.getimag() * B.getimag())/square);
    C.setimag((A.getimag() * B.getreal() - A.getreal() * B.getimag())/square);
    return C;
}

int main()
{
    complex c1, c2(15.5, 23.1);
    c1 = c2 + 13.5;
    c1 = 13.5 + c2;
    return 0;
}
这个例子是以顶层函数的形式定义操作符重载函数。我们同样来看主函数,其中定义了 c1 和 c2 两个 complex 类对象。先来看一下语句c1 = c2 + 13.5;,这个语句可以理解如下:
c1 = operator+(c2, 13.5);
因为我们在顶层函数中定义了complex operator+(const complex & A, const complex &B)函数,系统在执行c1 = operator+(c2, 13.5);时找到了对应的顶层函数,虽然参数不对,但可以通过类的构造函数将 13.5 转换成 complex 类对象,如此就满足 operator+() 函数的调用条件了,故而这一句是没有问题的。

我们再来看一下语句c1 = 13.5 + c2;,这一语句可以理解为:
c1 = operator+(13.5, c2);
这一句的执行与c1 = operator+(c2, 13.5);是一样的,它可以利用类的构造函数将 13.5 转换为 complex 类对象,因此这一句也是可以正确执行的。

结合例 3 和例 4,我们不难体会出以顶层函数形式重载操作符的优势。总结一下,以类成员函数的形式进行操作符重载,操作符左侧的操作数必须为类对象;而以顶层函数的形式进行操作符重载,只要类中定义了相应的转型构造函数,操作符左侧或右侧的操作数均可以不是类对象,但其中必须至少有一个类对象,否则调用的就是系统内建的操作符而非自己定义的操作符重载函数了。

在例 4 中,我们以顶层函数的形式进行操作符重载,但是因为无法直接访问 complex 类中的私有成员,故而在类中增添了 getimag()、getreal()、setimag() 和 setreal() 函数以操作类中的私有成员变量,如此一来实现这些操作符重载函数看上去就有些复杂了,不是那么直观。除了此种方法以外,我们还可以将 complex 类中的私有成员 real 和 imag 声明为 public 属性,但如此一来就有悖类的信息隐藏机制了。除了这两种方法外,我们是否还有其它方法解决这个问题呢?

答案是肯定的,还有一种方法,前面章节我们介绍过友元函数,如果我们将操作符重载函数这些顶层函数声明为类的友元函数,那么就可以直接访问类的私有成员变量了。

【例 5】
#include <iostream>
using namespace std;

class complex
{
public:
    complex();
    complex(double a);
    complex(double a, double b);
    friend complex operator+(const complex & A, const complex & B);
    friend complex operator-(const complex & A, const complex & B);
    friend complex operator*(const complex & A, const complex & B);
    friend complex operator/(const complex & A, const complex & B);
    void display()const;
private:
    double real;   //复数的实部
    double imag;   //复数的虚部
};

complex::complex()
{
    real = 0.0;
    imag = 0.0;
}

complex::complex(double a)
{
    real = a;
    imag = 0.0;
}

complex::complex(double a, double b)
{
    real = a;
    imag = b;
}

//打印复数
void complex::display()const
{
    cout<<real<<" + "<<imag<<" i ";
}

//重载加法操作符
complex operator+(const complex & A, const complex &B)
{
    complex C;
    C.real = A.real + B.real;
    C.imag = A.imag + B.imag;
    return C;
}

//重载减法操作符
complex operator-(const complex & A, const complex &B)
{
    complex C;
    C.real = A.real - B.real;
    C.imag = A.imag - B.imag;
    return C;
}

//重载乘法操作符
complex operator*(const complex & A, const complex &B)
{
    complex C;
    C.real = A.real * B.real - A.imag * B.imag;
    C.imag = A.imag * B.real + A.real * B.imag;
    return C;
}

//重载除法操作符
complex operator/(const complex & A, const complex & B)
{
    complex C;
    double square = A.real * A.real + A.imag * A.imag;
    C.real = (A.real * B.real + A.imag * B.imag)/square;
    C.imag = (A.imag * B.real - A.real * B.imag)/square;
    return C;
}

int main()
{
    complex c1(4.3, -5.8);
    complex c2(8.4, 6.7);
    complex c3;
   
    c3 = c1 + c2;
    cout<<"c1 + c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 - c2;
    cout<<"c1 - c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 * c2;
    cout<<"c1 * c2 = ";
    c3.display();
    cout<<endl;

    c3 = c1 / c2;
    cout<<"c1 / c2 = ";
    c3.display();
    cout<<endl;

    return 0;
}
本例就是采用友元函数的形式进行操作符重载,如此实现既能继承操作符重载函数是顶层函数的优势,同时又能够使操作符重载函数实现起来更简单。