前言
前一章我们继续学习基于对象编程之重载操作符。之前也接触过重载,重载的概念是重新赋予新的含义,如函数的重载,就是对一个已有的函数赋予新的含义,使之重新实现新的功能,以此,一个函数名就可以用来代表不同功能的函数,也就是函数名的一名多用。
其实运算符也可以重载,这也是c++的一大特色,实际上,我们不知不觉中就使用了运算符的重载,如运算符“+”,对于不同的数据均可以使用,是因为c++已经对运算符“+”进行了重载,是“+“能适应int、float、double类型的不同运算。
又如前面的运算符”<<“,本来是c++中的位运算中的左移运算符,,现在变成输出操作中和流对象配合使用的流插入运算符,同理”>>“,c++编译系统对他们进行重载的处理时放在头文件stream中,所以要使用<<和>>作为流插入运算符和流提取运算符,需要添加头文件stream以及using namespace std;
这一节我们讨论的时如何自己根据需要对c++已提供的运算符进行重载,赋予他们新的含义,实现运算符的一名多用!!!!!
一、为什么要运算符重载?
为了对象的运算直观方便,人们可以很直观的像操作普通数据类型一样操作对象!!!,例如需要定义专门的函数add()来进行加法运算,现在只需在是调用出使用”+“即可。
二、运算符重载的实质和目的
1.实质:运算符重载的实质就是函数的重载!
运算符重载的方法时定义一个重载运算符函数,使指定的运算符不仅能实现原有的功能,而且能实现函数中指定的新的功能,在使用被重载的运算符时,编译系统根据运算符附近的数据类型决定调用哪个函数,所以运算符的重载是通过定义函数实现的。-------运算符重载的实质就是函数的重载!
2.目的:重载的目的是使运算符用于对象,因为每个对象都是不同的,所以需要针对性重载!
3.定义
①函数类型 operator 运算符符号(形参表)
例如函数的原型(声明):
Complex operator + (int a, int b);
其中要注意operator +一起构成函数名,意思是”对运算符+重载的函数“,或者叫函数operator +重载了运算符+,operator是关键字,专门用来定义重载运算符函数的,+是c++中已有的、可以被重载的运算符(后面要讲哪些可以被重载)。从上面看出,其实我们要用函数的思想来理解运算符,只不过在使用的时候,c++编译系统自动调用了而已。
传统方法如下:
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
Complex complex_add(Complex &c2);
void display();
private:
double real;
double imag;
};
Complex Complex::complex_add(Complex &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
void Complex::display()
{
cout << "(" << real << " " << imag << "i" << ")" << endl;
}
int main(int argc, char* argv[])
{
Complex c1(2,-3), c2(5, 6), c3;
c3 = c1.complex_add(c2);
cout << "c1=";
c1.display();
cout << "c2=";
c2.display();
cout << "c3=";
c3.display();
return 0;
}
运算符重载的方法:
class Complex
{
public:
Complex(){real = 0; imag = 0;};
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)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
void Complex::display()
{
cout << "(" << real << " " << imag << "i" << ")" << endl;
}
int main(int argc, char* argv[])
{
Complex c1(2,-3), c2(5, 6), c3;
//此处通过打断点可以看出来就是c3 = c1.operator+(c2);
//对比你会发现就是上面所说的complex_add函数,这里就是operator+函数
//就是重载函数的调用进行计算,这里还涉及到对象的赋值和复制
c3 = c1 + c2;
cout << "c1=";
c1.display();
cout << "c2=";
c2.display();
cout << "c3=";
c3.display();
return 0;
}
②使用重载运算符的好处
从上面看出来,既然都是调用函数,那有上面好处呢,这里我们就需要从两个角度来考虑问题:
1)用户的角度:你是愿意使用c1.complex_add(c2),还是愿意使用c1 + c2,答案显而易见,使用、阅读和维护都方便;
2)类库的设计者角度:和用户是分离的,.h文件时接口文件。
综上所述,调用运算符的哪个函数是根据运算符两侧的数据类型所决定的(单目为一侧),如3+5调用整数加法重载函数。本来c++提供的运算符只能用于标准类型的数据的运算,但是c++的重要基础是类和对象,如果c++的运算符都无法用于对象,那类和对象不是很受影响,为了解决这个问题,使类和对象有更强的生命力,c++不是为类对象定义一大堆函数或者新的运算符,而是利用本来的运算符,这样用户用起来也习惯,一致性高,相当于扩展了运算符的作用范围。
把类和运算符重载结合起来(类中定义),可以在c++程序中定义出很有实际意义和实用方便的数据类型(类),针对这个类的对象,可以使用它的重载的运算符,如上c1.c1.operator+(c2),这也是c++的一大特色。
4.哪些运算符可以重载以及重载的原则
①哪些运算符不能重载
1).(成员与那算符)-----保证访问成员的功能不能被改变
2)*(成员指针访问运算符)-----保证访问成员的功能不能被改变
3)::(域运算符)-----他的运算对象是类型而不是变量或者一般表达式,不具备重载的功能
4)sizeof(长度运算符)-----他的运算对象是类型而不是变量或者一般表达式,不具备重载的功能
5)?:(条件运算符)----不知道为什么
②重载的原则(只能重载C++已有的运算符,而且要和原来的意思一样)
1)最好和原有的运算符功能含义一致,不引起歧义,运算符的操作数必须和原来一样,不然编译出错,这和使用时的状态是一直的,如c1+c2,你总不能对+用三个操作数吧;
2)运算符重载的主要目的是使c++的运算符能用于类对象(原来只能用于标准类型),所以原则上用于类对象的运算符必须重载,但是有两个例外(=和&),他俩是系统默认帮新声明的类默认重载好了,不用我们再重载,除非你需要,类似于构造函数,这就和前面对象的赋值对应上了;
3)重载的运算符必须和用户自定义类型的对象一起使用,其参数至少有一个是类对象或者是类对象的引用。
5.重载函数的实现方式
①作为类的成员运算符重载函数
c1 + c2 ----------------编译系统把他翻译成c1.operator + (c2)
②作为类的友元运算符重载函数(可以是普通函数,也可以是其他类的函数?,因为要访问类对象中的数据成员,所以必须是友元函数)
c1 + c2 ----------------编译系统把他翻译成operator + (c1, c2)
思考:什么时候用①,什么时候用②?
1)使用①时运算符的左侧必须时对象,而且这样使用默认会有this指针,就少写一个函数参数,而且第一个参数就是c1,是c++规定的,所以如果是双目运算符,则重载函数的参数只能有一个,不然编译提示参数过多,至于重载函数的其他参数(单目就没有)可以是普通类型变量或者引用,也可以是本类的类型对象或者引用,或者其他类的类型对象或者引用,函数的返回值可以任意,但是一般为本类的类型,据说这样才有意义,如下代码所示,如果是使用②的话,就需要明白的表示函数参数:
//使用其他类的成员函数作为本类的友元函数法
class Complex1;
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
int operator +(Complex1 &);
int operator +(int i);
void display();
private:
double real;
double imag;
};
class Complex1
{
public:
Complex1(){real = 1; imag = 1;};
Complex1(double r, double i){real = r; imag = i;};
friend int Complex::operator +(Complex1 &);
private:
double real;
double imag;
double tt;
};
//注意函数的定义要放在这里,不然就是提前引用,也会编译报错,不知道Complex1是什么
int Complex::operator +(Complex1 &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.tt;
return 1;
}
int Complex::operator +(int i)
{
int c;
c = i;
return c;
}
void Complex::display()
{
cout << "(" << real << " " << imag << "i" << ")" << endl;
}
int main(int argc, char* argv[])
{
Complex c1(2,-3), c2(5, 6), c3;
Complex1 tt;
int i;
i = c1 + tt;
cout << "c1=";
c1.display();
cout << "c2=";
c2.display();
cout << "c3=";
c3.display();
return 0;
}
2)注意在运算符的左侧必须是对象,如何使普通类型是不对的,具体原因如下伪码所示
Complex Complex::operator +(int i)
{
Complex c;
c.real = real + i;
return c;
}
c3 = c2 + i;//正确
c3 = i + c2;//不正确,这里的operator + 是Complex类的成员函数,如果要这么做,必须用友元函数法
3)因为友元的使用会破坏类的封装,所以一般使用成员函数法,但是有一些默认习惯可以参考:
双目运算符一般作为友元函数;
流插入<< 、流提取>>、 类型转换只能作为友元函数;
其他的作为成员函数。
三、各类运算符重载
1.双目运算符
2.单目运算符
①单目运算符要注意++和–,这两个运算符是分为前置和后置,例如:
Time operator++();//声明前置自增运算符++的重载函数
Time operator++(int);//声明后置自增运算符的重载函数
3.流插入运算符<< 和 流提取运算符 >>
①c++流插入运算符和流提取运算符是c++编译系统在类库中提供的,c++编译系统已经对他们进行了重载,可以输出和输入标准类型的数据(int等等);
②所有的c++编译系统都提供istream和ostream的类,cin和cout是他们的类对象,已经定义好了,只需要include即可;
③如果用户向使用"<<“和”>>"输出自己定义的数据类型(类),需要对他们进行重载,格式如下所示,因此只能将他们定义成欧元函数,而不能是成员函数(第一参数是this,这个不是)。
istream& operator>>(istream&, 自定义类&);
ostream& operator>>(ostream&, 自定义类&);
//重载流插入运算符<<
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
Complex operator +(Complex &);//运算符+重载为成员函数
friend ostream & operator << (ostream &, Complex &);//运算符<<重载为友元函数
void display();
private:
double real;
double imag;
};
Complex Complex::operator +(Complex &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
//注意此处的函数类型和参数类型均是&引用,不是局部变量,是实参的别名
ostream & operator << (ostream & output, Complex & c2)
{
//此处解释为out << "(" << c3.real << "+" << c3.imag << "i)" << endl;
//注意这个地方的<<是c++定义的标准数据流插入重载运算符
output << "(" << c2.real << "+" << c2.imag << "i)" << endl;
//此处解释为return cout;
return output;//此处返回的目的说是返回cout的当前值以便连续输出?????
}
int main()
{
Complex c1(2,4), c2(6,10), c3;
c3 = c1 + c2;
cout << c3;//此处编译系统解释为operator << (out, c3);
return 0;
}
四、运算符重载的总结
1.重要性
①c++中,运算符的重载是很重要的,很有实用意义的,他使类的设计更加的丰富,扩大了类的功能和使用范围,使程序易于设计、理解和维护,易于对对象的操作,体现了方便用户的思想;
②有了运算符重载之后,人们可以将用于标准类型的运算符用于自己声明的类,我们都知道,类的声明往往使一劳永逸的,有了好的类,用户在程序中就不必定义许多成员函数去完成某些运算和输入和输出的功能,使程序更加简单易读,好的运算符重载体现面向对象的思想。
2.具体做法
①先确定要重载哪个运算符,用于哪个类,不是一个运算符重载就可以适用于所有的类;
②设计运算符重载函数和有关的类(成员函数和友元函数);
③实际的工作中,有人把本领域或者本单位需要的运算符进行重载,把他们放在一个头文件中供大伙使用,用户只需要在头文件中看看函数的原型就知道怎么用了,如果自己想用的功能没有,那就自己写一个,记得要保存起来方便后续使用。
3.特殊注意
①在以上的编程中我们发现,使用引用的重要性,他能不用值传递的方式进行虚实结合,而通过默默的通过传递地址的方式使形参称为实参的别名,因此不生成临时变量(实参的副本),减少了时间和空间的开销;
②主要引用作为返回值时,代表的不是常量,而是引用代表的对象,他可以出现在赋值好的左边成为左值,可以被赋值或参与其他的操作,但是要小心,修改了引用就代表修改了他所代表的对象;
③很多运算符都可以重载,自己把握。
五、不同类型数据间的转换的总结
1.标准类型数据的转换
①隐式类型转换
int i = 6;
i = 7.5 + i;
7.5不加f,编译系统当作都变了处理,先将i的值6转换成double,然后与7.5相加得到13.5,向整形变量i赋值时,将13.5转换成整数13,然后赋值给i;
这种转换是编译系统自动完成,无须用户干涉,成为隐式类型转换。
②显示类型转换
c++:类型名(数据)
c:(类型名)数据
注意:c++保留了c的用法,使用(类型名)数据,但是提倡用 类型名(数据)
③思考:
上面的都是标准类型数据的转换,现在用户自己定义了类,编译系统是否能够转换呢,列如:一个自定义类的对象能否转换成标准类型?一个类的对象能否转换成另一个类的对象?一个标准类型数据能否转换成一个自定义类的对象?
答案是否定的,对于标准类型数据的转换,编译系统知道该这么转换,因为编译系统软件做好了,对于用户自己定义了类差别千方各异,编译系统不知道怎么处理,这个时候就需要定义专门的函数告诉编译系统怎么处理!!!!!
2.自定义类型数据的转换
①转换构造函数法:将一个其他类型的数据转换成一个类的对象(包括类对象到类对象)
1)学过的构造函数
Complex();//默认构造函数,没有参数
Complex(double r, double r);//用于初始化的构造函数,形参一般有两个以上的参数
Complex(Complex &c);//用于复制对象的构造函数,形参是本类对象的引用
Complex(double r){real = r; imag = 0;};//转换构造函数, 需要转换的类型是double,转换成Complex的对象
用户可以根据需要定义构造函数,在函数体中告诉编译系统如何转换,以上的构造函数可以同时出现在一个类中,他们是构造函数的重载,编译系统会根据参数的个数和类型进选择与之匹配的构造函数。
2)语法
Comlpex c1(3.5);//建立对象c1,由于只有一个参数,所以调用转换构造函数
Comlpex(3.5);//建立无名的对象,合法,但是无法使用,相当于将2.5转换成一个类对象,其实质就是一个构造函数,具体怎么操作要看函数体
c1 = Comlpex(3.5);//假设c1已经被定义为Complex对象,建立无名的对象,并赋值给c1
3)用法
//如果对运算符+进行了重载,使之能进行两个复数相加,此处会出错
//不能进行复数和浮点数相加
c = c1 + 2.5;//错误
//类似int(2.5),此处也是强制类型转换,只不过前者是标准类型强制转换,
//这里是标准类型转类对象,也可以是类对象转类对象(后面会讲)
c = c1 + Complex(2.5);//正确
4)小结
a.转换构造函数就是构造函数,只不过是参数只有一个,而且这个参数就是要转换成的参数而已,至于函数体最好是和转换相关,不然不叫转换构造函数,就是普通构造函数而已,所以函数体一定要有意义—实质就是一个只有一个参数的构造函数,恰好函数体里面实现类型转换,就变成了转换构造函数
b.不仅可以将标准类型转换成类对象,也可以将其他类对象转换成另一个类的对象,如
Teacher(Student &){num = s.num};
②类型转换函数法:将一个类的对象转换成一个其他类型的数据(不包括类对象到类对象)
1)语法:没有函数类型,没有参数,函数名就是operator 类型名(类似于运算符重载operator +的函数名),其返回值的类型就是类型名,如下double,所以看的出来只能作为成员函数,不能是普通函数。
operator 类型名()
{实现转换的语句}
//如果已经声明一个Complex的类,可以在类中定义这样一个类型转换函数
operator double()
{
return real;
}
2)含义:Complex类对象不是一律都转换成double类型数据,是根据表达式上下文来决定的。转换构造函数和类型转换函数都有一个共同的功能,当需要的时候,编译系统会自动调用这些函数,建立一个无名的临时对象(或者临时变量)。
d1 = d2 + c1;
//若已定义d1,d2为double型变量,c1,c2为Complex类对象,
//如类中已经定义了类型转换函数,编译系统发现+的左侧d2是double型,而右侧的c1是
//Complex类对象,如果没有+运算符重载,就会检查有无类型转换函数,结果发现了有对
//double的重载函数,就调用函数把Complex类对象c1转换为double型数据,建立了一个
//临时的double数据,并与d2相加,最后将一个double的值赋给d1
c2 = c1 + d2;
//若已定义d1,d2为double型变量,c1,c2为Complex类对象,
//如类中已经定义了转换构造函数并且重载+运算符(Complex的友元函数)
//编译系统发现+的左侧的c1是Complex类对象,右侧d2是double型
//有+运算符重载,但是要求两个CompleX的对象,没有double类型转换函数
//最后有构造函数的重载函数,就调用Complex(d2),建立一个临时的Complex类对象,
//再调用operator +函数,将两个复数相加,然后赋给c2,相当于:
c2 = c1 + Complex(d2);
//类型转换函数
//实现将一个double的数据和Complex对象相加
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
operator double()
{
return real;
};
private:
double real;
double imag;
};
int main()
{
Complex c1(2,4), c2(6,10), c3;
double i = 5.0, j;
c3 = c2;//没有转换,直接对象赋值
//以下两者一致,先看有无运算符重载,再看转换构造函数,最后类型转换运算符重载函数(类型转换运算符重载函数)
//相当于运算符重载一种
// j = i + c1;
j = c1 + i;
cout << j << endl;
return 0;
}
3)小结:
在已经定义相应转换构造函数情况下,将运算符+函数重载为友元函数,在两个复数相加时,可以使用交换律;
如果将+运算符重载函数为类的成员函数时,交换率不适用,其中涉及到this,若果要用,只能再重载友元函数作为运算符重载。
//类型转换函数,转换构造函数和运算符重载
//实现将一个double的数据和Complex对象相加
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
Complex(double r){real = r;};//转换构造函数
Complex operator +(Complex &c2);//成员函数,运算符重载函数
friend double operator +(double d, Complex &c);//友元函数,运算符重载函数
friend double operator +(Complex &c, double d);//友元函数,运算符重载函数
// operator double()//类型转换函数
// {
// return real;
// };
private:
double real;
double imag;
};
Complex Complex::operator +(Complex &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
double operator +(double d, Complex &c)
{
return (c.real + d);
}
double operator +(Complex &c, double d)
{
return (c.real + d);
}
int main()
{
Complex c1(2,4), c2(6,10), c3;
double i = 5.0, j;
//以下具体怎么执行,先运算符重载,再类型转换,最后转换构造
// j = i + c1;
// j = c1 + i;
c2 = i + c1;
c2 = c1 + i;
cout << j << endl;
return 0;
}
**4)好处:**假如程序中需要对一个Complex类对和一个double型的变量进行+,-,,/等运算,先用类型转换函数变成double,之后再用C++提供的标准运算符进行标准数据进行+,-,,/,而不需要对每个运算符进行重载。
总结
这里对一个含有对象、变量以及运算符的语句,编译系统如何处理的呢?处理一般经过如下步骤:
1)先看有无运算符重载;
2)有无类型转换类型;
3)最后有无转换构造函数。
//列题1,不用成员函数,不用友元函数,实现两个复数相加
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
//private:
double real;
double imag;
};
//从这里可以看出来,运算符重载不需要在类中也可以,主要是operator+这个东西
void operator +(Complex &c1, Complex &c2)
{
c1.real += c2.real;
c1.imag += c2.imag;
}
int main()
{
Complex c1(2,4), c2(6,10), c3;
c1 + c2;
return 0;
}
//列题2,实现两个复数加减乘除,成员函数的方法
class Complex
{
public:
Complex(){real = 0; imag = 0;};
Complex(double r, double i){real = r; imag = i;};
Complex operator+(Complex&c2);
Complex operator-(Complex&c2);
Complex operator*(Complex&c2);
Complex operator/(Complex&c2);
private:
double real;
double imag;
};
Complex Complex::operator+(Complex&c2)
{
real += c2.real;
imag += c2.imag;
return (*this);
}
Complex Complex::operator-(Complex&c2)
{
real -= c2.real;
imag -= c2.imag;
return (*this);
}
Complex Complex::operator*(Complex&c2)
{
Complex c3;
c3.real = real*c2.real - imag*c2.imag;
c3.imag = imag*c2.real + real*c2.imag;
return c3;
}
Complex Complex::operator/(Complex&c2)
{
return (*this);
}
int main()
{
Complex c1(2,4), c2(6,10), c3;
c3 = c1 + c2;
c3 = c1 - c2;
c3 = c1 * c2;
c3 = c1 / c2;
return 0;
}