目录
所谓的重载就是重新赋予名字新的意义。运算符也是一样的,重新赋予运算符新的意义。那么既然赋予了运算符新的意义,那么编译器是如何区分它们的呢?
其实重载运算符的方法是定义一个重载运算符的函数,使原有的运算符增加新的功能。所以运算符重载实质上是函数的重载。
重载运算符的函数一般格式:
重载类型 operator 运算符名称(形参表){ 对运算符的重载操作 }
例如将“+”重载用于两个复数的相加,函数的原型如下:
Complex operator+(Complex& c1,Complex& c2);
这里的Complex是我们定义的复数类,也是我们重载函数的返回类型。
下面看一道例题,来详细了解一下运算符的重载
题目要求通过重载“+”实现两个复数的相加,下面看一下代码:
#include <iostream>
using namespace std;
class complex
{
public:
complex()=default;//默认构造函数
complex(double r,double i){real=r;imag=i;}//构造函数的重载
complex operator+(complex &c2);//运算符重载的函数原型
void display();
private:
double real;
double imag;
};
complex complex::operator+(complex &c2)//为了将c2的值传回主函数,这里用了引用
{
return complex(real+c2.real,imag+c2.imag);
}
void complex::display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main()
{
complex c1(3,4),c2(5,-10),c3;
c3=c1+c2;
cout<<"c1+c2=";c3.display();
return 0;
}
运行的结果如下:
c1+c2=(8,-6i)
在这里运算符的重载c3=c1+c2实质上就是调用了重载函数,c++的编译器自动将c1+c2解释为c1.operator(c2),这样我们就不难理解complex(real+c2.real,imag+c2.imag);因为重载函数声明在类体中,作为了类的成员函数,所以我们可以直接调用类体的私有数据,因为operator这个函数是在c1的作用域下,实际调用的就是c1的real,c2是operator的形参,可以用c2.real的形式调用,同理虚部也是一样的,这样我们就完成了两个复数的相加。
这里我们可以想,既然重载运算符的函数可以作为类的成员函数,那么重载运算符的函数是否也能作为类的友元函数来出现呢?
既然作为友元函数来出现,那么这个重载函数一定出现在类体外。这样就需要两个形参c1、c2
下面是实现代码:
#include <iostream>
using namespace std;
class complex
{
public:
complex()=default;
complex(double r,double i){real=r;imag=i;}
friend complex operator+(complex &c1,complex &c2);//这里声明为友元函数
void display();
private:
double real;
double imag;
};
complex operator+(complex &c1,complex &c2)//普通成员函数不需要作用域
{
return complex(c1.real+c2.real,c1.imag+c2.imag);
}
void complex::display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main()
{
complex c1(3,4),c2(5,-10),c3;
c3=c1+c2;
cout<<"c1+c2=";c3.display();
return 0;
}
我们在将运算符+重载为非成员函数之后,编译器对c1+c2的解释变成operator+(c1,c2);实质上就是调用了:
complex operator+(complex &c1,complex &c2)
{
return complex(c1.real+c2.real,c1.imag+c2.imag);
}
既然运算符重载函数可以作为类的成员函数,也可以作为类的友元函数,那么什么情况下用成员函数的方式?什么情况下用友元的形式?
如果将运算符重载函数作为成员函数,那么它会通过this指针的方法来访问类中的数据成员,那么相较于友元的形式,我们就可以少写一个函数的参数。
- 根据习惯我们一般将双目运算符重载为友元函数,将单目运算符和复合运算符(+=,-=,/=,*=,&=,!=,^=,%=,>>=,<<=)重载为成员函数。 对于像流插入“<<”和流提取运算符“>>”、类型转换运算符不能定义为类的成员函数,只能作为友元函数。
重载运算符的规则
1. c++不允许用户自己定义新的运算符,只能对已有的c++运算符进行重载。
2. c++绝大多数运算符允许重载,不能重载的有5个:
- . (成员访问运算符)
- * (成员指针访问运算符)
- :: (域运算符)
- sizeof (长度运算符)
- ?: (条件运算符)
3. 重载不能改变运算符的运算对象(即操作数)的个数。
4. 重载不能改变运算符的优先级别。
5. 重载不能改变运算符的结合性。
6. 重载运算符不能有默认参数。
7. 重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象。(或类对象的引用)
8. 用于类对象的运算符一般必须重载,但有两个例外,运算符"="和"&"不必用户重载。
9. 理论上讲,可以将一个运算符重载为执行任意的操作。但是如果违背了运算符重载的初衷,会导致程序不容易理解。所以,应该使运算符的功能类似于该运算符作用于标准类型数据时所实现的功能。
在前面我们实现了两个复数的相加,如果想让一个复数和一个整数相加c1+i,可以重载+作为成员函数,如下面的形式:
Complex Complex::operator+(int &i){return Complex(real+i,imag);}
注意表达式中重载的运算符+左侧应为Complex类对象,如c3=c2+i;不能写成c3=i+c2;因为+左侧的i是一个整数,这时我们就无法调用operator+函数,如果我们想访问类的私有成员,则必须声明为友元函数。
friend operator+(int &i,Complex &c);
将双目运算符重载为友元函数时,由于友元函数不是该类的成员函数,因此在函数的形参列表中必须有两个参数,不能省略。
重载单目运算符
由于单目运算符只有一个操作数,因此运算符重载函数作为成员函数则可以省略此参数。
下面以自增运算符++为例,介绍单目运算符的重载。
有一个Time类,包含数据成员minute(分)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值。
参考代码:
#include <iostream>
using namespace std;
class Time
{ public:
Time( ){minute=0;sec=0;} //默认构造函数
Time(int m,int s):minute(m),sec(s){ } //构造函数重载
Time operator++( ); //声明运算符重载函数
void display( ){cout<<minute<<″:″<<sec<<endl;} //定义输出时间函数
private:
int minute;
int sec;
};
Time Time∷operator++( ) //定义运算符重载函数
{ if (++sec>=60)
{ sec-=60; //满60秒进1分钟
++minute;
}
return *this; //返回当前对象值
}
int main( )
{ Time time1(34,0);
for (int i=0;i<61;i++){
++time1;
time1.display( ); }
return 0;
}
运行情况如下:
34:1
34:2
┆
34:59
35:0
35:1 (共输出61行)
我们可能有一个疑问,++有两种使用方式,前置自增运算符和后者自增运算符,它们的作用是不一样的,我们如何在重载时区分两者呢?
针对“++”和“--”这一特点,c++约定:在自增(自减)运算符重载函数中,增加一个int型形参,就是后置自增(自减)运算符函数。
在上一个程序的基础上增加对后置自增运算符的重载。修改后的程序如下:
#include <iostream>
using namespace std;
class Time
{ public:
Time( ){ minute=0;sec=0;}
Time(int m,int s):minute(m),sec(s){}
Time operator++( ); //声明前置自增运算符“++”重载函数
Time operator++(int); //声明后置自增运算符“++”重载函数
void display( ){cout<<minute<<″:″<<sec<<endl;}
private:
int minute;
int sec;
};
Time Time∷operator++( ) //定义前置自增运算符“++”重载函数
{ if(++sec>=60)
{ sec-=60;
++minute; }
return *this; //返回自加后的当前对象
}
Time Time∷operator++(int) //定义后置自增运算符“++”重载函数
{ Time temp(*this);
sec++;
if( sec>=60 )
{ sec-=60;
++minute;
}
return temp; //返回的是自加
if( sec>=60 )
{ sec-=60;
++minute;
}
return temp; //返回的是自加前的对象
}
int main( )
{ Time time1(34,59),time2;
cout<<″ time1 : ″;
time1.display( );
++time1;
cout<<″++time1: ″;
time1.display( );
time2=time1++; //将自加前的对象的值赋给time2
cout<<″time1++: ″;
time1.display( );
cout<<″ time2 :″;
time2.display( ); //输出time2对象的值
}
运行结果如下:
time1 : 34:59 (time1原值)
++time1: 35:0 (执行++time1后time1的值)
time1++: 35:1 (再执行time1++后time1的值)
time2 : 35:0 (time2保存的是执行time1++前time1的值)
可以看到:重载后置自增运算符时,多了一个int型的参数,增加这个参数只是为了与前置自增运算符重载函数有所区别,此外没有任何作用。编译系统在遇到重载后置自增运算符时,会自动调用此函数。
重载流插入运算符“<<”和流提取运算符“>>”
对“<<”和“>>”重载的函数形式如下:
istream & operator >> (istream &,自定义类 &);
ostream & operator << (ostream &,自定义类 &);
我们可以在实现两个复数相加的基础上实现复数的输出,代码如下:
#include <iostream>
using namespace std;
class Complex
{ public:
Complex( ){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
Complex operator + (Complex &c2); //运算符“+”重载为成员函数
friend ostream& operator << (ostream&,Complex&);
//运算符“<<”重载为友元函数
private:
double real;
double imag;
};
Complex Complex∷operator + (Complex &c2) //定义运算符“+”重载函数
{ return Complex(real+c2.real,imag+c2.imag); }
ostream& operator << (ostream& output,Complex& c)
//定义运算符“<<”重载函数
{
output<<″(″<<c.real<<″+″<<c.imag<<″i)″<<endl;
return output;
}
int main( )
{ Complex c1(2,4),c2(6,10),c3;
c3=c1+c2;
cout<<c3;
return 0;
}
运行结果:
(8+14i)
我们想一想return output的作用是什么?其实是能连续向输出流插入信息。output是ostream类的对象的引用(它是实参cout的引用,或者说output是cout的别名),cout通过传送地址给output,使它们两者共享一段相同的内存单元,因此,return output就是return cout,将输出流cout的现状返回,即保留输出流的现状。
注意区分一下什么情况下“<<”是标准类型数据流插入符,什么情况下是重载流插入符。如
cout<<c3<<5<<endl;
有下划线的是调用重载的流插入符,后面两个<<不是重载的流插入符,因为不是Complex类对象而是标准类型数据。
在运算符重载中使用引用(reference)的重要性。利用引用作为函数的形参可以在调用函数的过程中不是用传递值的方式进行虚实结合,而是通过传址方式使形参成为实参的别名,因此不生成临时变量(实参的副本),减少了时间和空间的开销。此外,如果重载函数的返回值是对象的引用时,返回的不是常量,而是引用所代表的对象,它可以出现在赋值号的左侧而成为左值(left value),可以被赋值或参与其他操作(如保留cout流的当前值以便能连续使用“<<”输出)。但使用引用时要特别小心,因为修改了引用就等于修改了它所代表的对象。