C++ 程序设计 第4章 运算符重载
第4章 运算符重载
1 运算符重载的概念
C++的表达式由运算符和操作数按照规则构成。比如算术运算符,+ - * / % 如果不做特殊处理,则这些算术运算符通常只能用于对基本数据类型的常量或变量进行运算,而不能用于对象之间的运算。将复数定义为一个对象,然后对复数进行加法,减法等操作。C++中提供了运算符重载机制,可以将像 + - 这样的运算符用于像复数这样的对象,解决对象之间的运算问题。
运算符重载,就是给已有的运算符赋予多重含义,使同一个运算符作用于不用类型的数据时产生不同的行为。运算符重载的目的是使得C++中的运算符也能够用来操作对象。C++允许重载大部分的内置运算符,可重载的运算符如下
双目算术运算符 | +,-,*,/,% |
关系运算符 | ==,!=,<,>,<=,>= |
逻辑运算符 | ||,&&,! |
单目运算符 | +(正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | |,&,~,^,<<,>> |
赋值运算符 | =,+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>= |
空间申请与释放 | new,delete,new[],delete [] |
其他运算符 | () (函数调用),->(成员访问),, (逗号),[] (下标) |
不可重载的运算符及符号列表
成员访问运算符 | . |
成员指针访问运算符 | .* ,->* |
域运算符 | :: |
长度运算符 | sizeof |
条件运算符 | ?: |
预处理符号 | # |
用于类运算的运算符通常都要重载。有两个运算符,系统提供了默认的重载版本,它们是赋值运算符 =
和地址运算符 &
,对于 =
,系统默认重载为对象成员变量的赋值。对于 &
,系统默认重载为返回任何类对象的地址。
运算符重载的实质是编写以运算符为名称的函数,使用运算符的表达式就被解释为对重载函数的调用。函数名由关键字 operator
和其后要重载的运算符符号构成。与其他函数一样,重载运算符有一个返回类型和一个参数列表。这样的函数称为运算符函数。格式如下
返回值类型 operator 运算符(形参表){
函数体
}
运算符可以被重载为全局函数,也可以被重载为类的成员函数。如果定义为全局函数,对于二元运算符,需要为函数传递两个参数,即函数的参数个数就是运算符的操作数个数,运算符的操作数就成为的实参。
如果定义为类的成员函数,对于二元运算符,则只需要传递一个参数,即函数的参数个数就是运算符的操作数个数减1,运算符的操作数有一个成为函数作用的对象,其余的成为成员函数的实参。声明为全局函数时,通常应是类的友元。
包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。
运算符可以被多次重载,一般来说,倾向于将运算符重载为类的成员函数,这样能够较好的体现运算符和类之间的关系。
重载运算符为类的成员函数
重载运算符 +
-
#include <iostream>
using namespace std;
class myComplex{
private:
double real,imag;
public:
myComplex();
myComplex(double r);
myComplex(double r,double i);
void outCom();
myComplex operator-(const myComplex &c);
friend myComplex operator+(const myComplex &c1,const myComplex &c2);
friend myComplex operator-(const myComplex &c1,const myComplex &c2);
};
myComplex::myComplex() {
real = 0;
imag = 0;
}
myComplex::myComplex(double r, double i) {
real = r;
imag = i;
}
void myComplex::outCom() {
cout << "(" << real << "," << imag << ")";
}
myComplex myComplex::operator-(const myComplex &c) {
return myComplex(this->real-c.real, this->imag-c.imag);
}
myComplex operator+(const myComplex &c1, const myComplex &c2) {
return myComplex(c1.real+c2.real, c1.imag+c2.imag);
}
myComplex::myComplex(double r) :real(r),imag(0){}
myComplex operator-(const myComplex &c1, const myComplex &c2) {
return myComplex(c1.real-c2.real,c2.imag-c2.imag);
}
int main(){
myComplex c1(1,2),c2(3,4),res;
c1.outCom();
cout << "operator+";
c2.outCom();
cout << "=";
res = c1 + c2;
res.outCom();
cout << endl;
c1.outCom();
cout << "operator-";
c2.outCom();
cout << "=";
res = c1 - c2;
res.outCom();
cout << endl;
// 友元函数的好处
res = c1 - 4;
res.outCom();
res = 4 - c1;//运算符重载成员函数无法执行
res.outCom();
return 0;
}
重载运算符为友元函数
有时,将运算符重载为成员函数时满足不了需要。重载运算符成员函数有弊端,比如上面的operator-重载运算符成员函数可以执行 c1-4,但却执行不了 4-c1。而使用友元函数时,需要将两个操作数都列出来,因此 c1-4 和 4-c1 都不会有问题。
通过运算符重载 +
-
实现了对象和基本数据类型变量的减法操作,当然,运算符重载也可以重载为全局函数,但访问不了类中的私有成员变量。
重载运算符的规则
- 重载后运算符的含义应该符合原有的用法习惯。例如,重载
+
运算符,完成的功能就应该类似于做加法,在重载的+
运算符中做减法是不合适的。 - 运算符重载不能改变运算符原有的语义,包括运算符的优先级和结合性。
- 运算符重载不能改变运算符操作数的个数及语法结构
- 不能创建新的运算符,即重载运算符不能超出C++允许重载的运算符范围
- 重载运算符
()
[]
->
=
时,只能将它们重载为成员函数,不能重载为全局函数 - 运算符重载不能改变该运算符用于基本数据类对象的含义。重载的运算符可以用于用户自定义类型的对象之间的运算,也可以用于用户自定义类型的对象与基本数据类型对象之间的混合运算。
2 重载赋值运算符
有时希望 =
两边的操作数的类型即使不赋值兼容也能够成立,这就需要对 =
进行重载,C++规定,=
只能重载为成员函数
重载后 s1 = s2
则是 s1.operator=(s2)
赋值运算符必须重载为成员函数
为了保持与通常意义下的赋值运算符的功能相一致,应该让重载的赋值运算符仍然能连续使用,即 res = c1 = c2
成立,所以 operator=
函数通常要返回引用。
浅拷贝和深拷贝
同类对象之间可以通过赋值运算符 =
互相赋值。如果没有经过重载, =
的作用就是将赋值号右侧对象的值一一赋值给左值的对象。这相当于值得拷贝,称为浅拷贝。
如果赋值的对象中涉及指针或是引用,则他们之间是互相关联的,一个值变化了,另一个值也跟着变化。因为对象中的指针指向的是同一个内存地址。
浅拷贝可能会带来程序上的错误。当对象p1消亡时,需要释放构造函数中动态申请的空间,也就是那个整型值占据的空间。而当对象p2消亡时,会试图释放这个空间。重复释放同一块空间会产生错误。
A a1;// 默认构造函数
A a2;// 默认构造函数
a2 = a1;//会丢弃a2初始化时创建的指针变量
重载赋值运算符后,赋值语句的功能是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方,这样的拷贝叫深拷贝。
3 重载流插入运算符和流提取运算符
<<
流插入 >>
流提取,他们都是C++类库中提供的。在类库提供的头文件中已经对 <<
和 >>
进行了重载,使之分别作为插入运算符和流提取运算符,能用来输出和输入C++基本数据类型的数据。cout 是 ostream类的对象,cin是 istream 类的对象,他们都是在头文件 iostream 中声明的。
对这两个运算符重载,使之用于自定义的类对象。他们的重载与其他运算符的重载类似,但操作符的左边是流对象的别名,而不是被操作的对象,运算符接在流对象的后面,他们要直接访问类的私有成员。而且流是标准类库,用户程序中只能继承不能修改,所以重载函数不能是流类库中的成员,而必须重载为类的友元。
一般格式
ostream &operator<<(ostream &output,类名 & 对象名){
return output;
}
istream &operator>>(istream &input,类名 & 对象名){
return input;
}
4 重载强制类型转换运算符
强制类型转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象
这个对对象进行强制类型转换的表达式等价于 对象.operator 类型名()
即变成对运算符函数的调用
重载强制类型转换运算符时,不需要指定返回值类型,因为返回值类型是确定的,就是运算符本身代表的类型。比如double
5 重载自增,自减运算符
自增运算符和自减运算符都可以被重载,但是他们有前置和后置之分。运算符重载相当于定义了一个以运算符为名字的函数。这样的函数不能区分前置及后置情况,C++规定,在重载 ++
--
时,允许写一个增加了无用int类型形参的版本,编译器处理 ++
--
前置的表达式时,调用参数个数正常的重载函数,处理后置的表达式时,调用多出一个参数的重载函数。
前置形式自增,自减运算符的返回值类型是引用,而后置形式自增,自减运算符的返回值是类型,这是因为运算符重载最好保持原运算符的用法,C++固有的前置自增,自减运算符的返回值本来就是操作数的引用,而后置形式的自增,自减运算符的返回值则是操作数值修改前的复制品。
小结
运算符重载,就是给已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时产生不同的行为。C++允许重载大部分的内置运算符。
用于类运算的运算符通常都要重载,有两个运算符,系统提供了默认的重载版本,赋值运算符 =
,和地址运算符 &
运算符重载的实质是编写以运算符为名称的函数,使用运算符的表达式就被解释为对重载函数的调用。函数名由关键字 operator 和其后要重载的运算符符号构成。
运算符可以被重载为全局函数,也可以被重载为成员函数。
重载后运算符的含义应该符合原有的用法习惯。
运算符重载不能改变运算符原有的语义,包括运算符的优先级和结合性。
运算符充值不能改变运算符操作数的个数及语法结构
不能创建新的运算符
重载运算符 ()
[]
->
或者 赋值运算符 =
时,只能将他们重载为成员函数,不能重载为全局函数。
运算符重载不能改变该运算符用于基本数据类型对象的含义。
如果没有经过重载,=
的作用就是将赋值号右侧对象的值一一赋值给左侧的对象。这相当于拷贝,称为浅拷贝
重载赋值运算符后,赋值语句的功能时将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝称为深拷贝。
程序员可以对流插入运算符和流提取运算符进行重载,使之用于自定义的类对象。重载函数不能是流类库的成员,而必须重载为类的友元。
类型的名称(包括类的名字)本身也是一种运算符,即强制类型转换运算符。强制类型转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数,经过适当重载后,(类型名)对象,这个对对象进行强制类型转换的表达式等价于
对象.operator 类型名()
自增运算符 ++
和自减运算符 --
都可以被重载,且有前置和后置之分,C++规定,在重载 ++
或 --
时,允许写一个增加了无用int类型形参的版本,编译器处理 ++
或 --
前置的表达式时,调用参数个数正常的重载函数,处理后置的表达式时,调用多于一个参数的重载函数。
当一元运算符的操作数,或者二元运算符的左操作数是该类的一个对象时,重载运算符函数一般定义为 成员函数
利用运算符重载实现集合 Set 类型。在类 Set 中重载运算符 +
(表示集合的并)、-
(表示集合的差)、*
(表示集合的交)、<
(判断是否是真子集)、==
(判断两个集合是否相等)、!=
(判断两个集合是否不相等),使用友元函数或成员函数方式实现。