第一节 运算符重载的基本概念
C++预定义的运算符,只能用于基本数据类型的运算。基本数据类型包括:整型、实型、字符型、逻辑型等。
在数学上,两个复数可以直接进行+、-运算,但是在C++中,直接将+、-用在复数对象是不允许的。
有时候也会希望让对象也能通过运算符进行运算,这样代码更简洁、更容易理解,这个时候就需要运算符的重载了。
运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象。
它的实质是函数重载。可以重载为普通函数,也可以成员函数。
把含运算符的表达式转换成对运算符函数的调用,把运算符的操作数转换成运算符函数的参数。
运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。
运算符重载的形式:
返回值类型 operator 运算符(形参表)
{
//函数体
}
如下例:
class Complex{
public:
double real,imag;
Complex(double r = 0.0, double i = 0.0):real(r),imag(i){ }
Complex operator-(const Complex & c);
};
Complex operator+(const Complex &a, const Complex &b){
return Complex(a.real + b.real,a.imag + b.imag);//返回一个临时对象
}
Complex Complex::operator-(const Complex &c)
{
return Complex(real – c.real, imag – c.imag);//返回一个临时对象
}
int main(){
Complex a(4,4),b(1,1),c;
c = a + b;//等价于c = operator+(a,b);
cout << c.real <<“,” <<c.imag << endl;
cout<<(a-b).real << “,” << (a-b).imag << endl;
//a-b等价于a.operator-(b)
return 0;
}注意:重载为成员函数时,参数个数为运算符目数减一(另一个就是我调用这个成员函数的对象);重载为普通函数时,参数个数为运算符目数。
第二节 赋值运算符的重载
1.赋值运算符“=”的重载
有时候希望赋值运算符两边的类型可以不匹配,此时就需要重载赋值运算符“=”。赋值运算符“=”只能重载为成员函数。
例程如下:
class String{
private:
char * str;//指向动态分配的数组
public:
String():str(new char[1]){str[0] = 0;}
const char * c_str(){return str;};
String & operator = (const char*s);
String::~String(){delete [] str;}
};
String &String::operator = (const char *s)
{
//重载=以使obj=”hello”能够成立
delete[] str;
str = new char[strlen(s)+1];
strcpy(str,s);
return * this;
}
int main()
{
String s;
s = “Good luck,”;//等价于s.operator=(“Good Luck,”);
cout<<s.c_str()<<endl;
//String s2 = “hello!”;//这条语句不注释掉就会出错
s = “Shenzhou 8!”;//等价于s.operator=(“Shenzhou 8!”);
cout <<s.c_str()<<endl;
return 0;
}
对重载函数进行解释。首先把str给delete掉,然后给str重新分配一个空间,大小为s字符串的大小+1,然后把s的值复制给str,返回一个本身的引用。要注意,“=”已经被重载,再编写String s2 = “hello!”后,=已经不是赋值语句,所以必然会出错。
2.相关其它内容
如下代码:
class String{
private:
char * str;//指向动态分配的数组
public:
String():str(new char[1]){str[0] = 0;}
const char * c_str(){return str;};
String & operator = (const char*s){
delete [] str;
str = new char[strlen(s)+1];
strcpy(str,s);
return * this;
};
String::~String(){delete [] str;}
};
我们在主函数里面要实现下面功能:
String S1,S2;
S1=”this”;
S2=”that”;
S1=S2;
在没有重载“=”的时候,S1=S2也可以编译通过,因为它们类型完全相同的。但是,这个“=”会使S1每一点都和S2一样。那么,这会有什么问题呢?让我们一步一步分解来看。
首先执行String S1,S2;S1=”this”;S2=”that”;那么就会实现这样的效果:
图2.1 S1=”this”;S2=”that”;
再执行S1=S2,我们发现成了这个样子:
图2.2 S1=S2的结果
即S1实际上是指向了S2,两者实际上只指向的一个,原来S1的空间失去了指向,与我们想让S1中的内容(所指向的空间的内容)和S2一样的想法完全不一样。
如果S1对象消亡,析构函数将释放 S1.str 指向的空间,则S2消亡时还要释放一次,相当于delete S2了两次。
如果执行S1=”other”,会导致S2.str指向的地方被delete掉。所以重载之后,可以避免这样的问题。
考虑下面的语句:
String s;
s = “Hello”;
s = s;
会有什么问题呢?我们重新看重载“=”的成员函数,发现函数第一句和就是把等号左侧对象的空间给delete掉了,这在普通的语句下没有什么问题,但是在这里,赋给等号左边对象值的那个对象也是它本身,这样delete掉之后,后面的strcpy函数就无法复制正确的值给左侧的对象了。为了解决这个问题,需要在这个重载成员函数的函数体开头添加以下语句:
if(this == &s)
return *this;
接下来对operator=的返回值类型进行讨论。当对运算符进行重载的时候,好的风格是应该尽量保留运算符原来的特性。
我们考虑a=b=c,若是void,那么b=c返回值类型就是void,就没办法再执行a=操作了,所以可以用String类型。
再考虑(a=b)=c。先执行a=b,在C++里面,执行=的返回值是左侧元素的引用,所以(a=b)的结果是一个a的引用,对a的引用赋值为c,那么这个b毫无用处。因此不能用String,而是用String &这样一个引用格式。this是当前对象的地址,那么*this就是当前对象,这解释了为什么要用*this的原因。
为String类编写复制构造函数的时候,会面临和“=”同样的问题(两个对象指向同一个空间),用他同样的方法处理:
String(String &s)
{
str = new char[strlen(s.str)+1];
strcpy(str,s.str);
}
关于浅拷贝和深拷贝待补充
第三节 运算符重载为友元
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数并不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
如下代码:
class Complex
{
double real,imag;
public:
Complex(double r, double i):real(r),imag(i){};
Complex operator+(double r);
};
Complex Complex::operator+(double r)
{
return Complex(real+r,imag);
}
经过重载以后,c=c+5有意义,相当于c=c.operator+(5)
但是,c=5+c就会出错。所以,为了使得上述表达式成立,需要将+重载为普通函数,这样c=5+c就可以通过了。但是普通函数又不能访问私有成员,即不能计算c=c+5。这样,我们只能用重载为友元函数了。如下:
class Complex
{
double real,imag;
public:
Complex(double r, double i):real(r),imag(i){};
friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator+(double r)
{
return Complex(real+r,imag);
}
第四节 运算符重载实例:可变长整型数组
如下代码:
int main(){//要编写可变长整型数组类,使之能如下使用
CArray a;//开始数组是空的
for(int i = 0; i < 5;++i)
a.push_back(i);//要用动态分配的内存来存放数组元素需要一个指针成员变量
CArray a2,a3
a2 = a;//要重载”=”,把a中的值复制给a2
for(int i = 0; i<a.length;++i)
cout<<a2[i]<<“”;//要重载[],因为a2原来是一个对象
a2 = a3;//a2是空的,因为原来的空间被释放了
for(int i = 0;i<a2.length;++i)//a2.length()返回0
cout<<a2[i]<<“”;
cout<<endl;
a[3]=100;
CArray a4(a);
CArray A4(A);//要自己写复制构造函数
for(int i = 0; i<a4.length;++i)
cout<<a4[i]<<“”;
return 0;
}
class CArray{
int size;//数组元素的个数
int *ptr;//指向动态分配的数组
public:
CArray(int s = 0);//s代表数组元素的个数
CArray(CArray &a);
~CArray();
void push_back(int v);//用于在数组尾部添加一个元素v
CArray & operator=(const CArray &a);
//用于数组对象间的赋值
int length(){return size;}//返回数组元素个数
int & CArray::operator[](int i)
//返回值不能为int,不支持a[i]=4,双目运算符,但是在类内,只有一个运算符
{//用以支持根据下标访问数组元素,如n=a[i]和a[i]=4这样的语句
return ptr[i];
}
};
CArray::CArray(int s):size(s)
{//构造函数
if(s ==0)
ptr = NULL;
else
ptr = new int[s];
}
CArray::CArray(CArray &a){//复制构造函数,要实现深复制
if(!a.ptr){
ptr = NULL;
size = 0;
return;
}
ptr = new int [a.size];
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size = a.size;
}
CArray::~CArray()
{
if(ptr) delete [] ptr;
}
CArray & CArray::operator=(const CArray &a)//深拷贝,而不是浅拷贝
{//赋值号的作用是使“=”左边对象里存放的数组,大小和内容都和右边的对象一致。
if(ptr == a.ptr)
return *this;//防止前文所述的出错
if(a.ptr == NULL){//如果a里面的数组是空的
if(ptr) delete[] ptr;
ptr = NULL;
size =0;
return *this;
}
if(size <a.size){//如果原有空间不够,则新建一个足够大的空间
//如果足够大,就不分配新的空间直接执行if后面的语句
if(ptr)
delete [] ptr;
ptr = new int[a.size];
}
memcpy(ptr,a.ptr,sizeof(int)*a.size);//空间大小为数目*一个int的字节数
size = a.size;
return *this;
}
void CArray::push_back(int v)
{//在数组尾部添加一个元素。先判断原来是否有元素,如果有元素,就新建一个临时空间,
//然后把原来的元素复制过来,然后删除原来的空间,然后把ptr指针指向了tmpPtr这个临时空间
//这个元素非常浪费资源
if(ptr){
int *tmpPtr = new int[size+1];//重新分配空间
memcpy(tmpPtr,ptr,sizeof(int)*size);//拷贝原数组内容
delete[] ptr;
ptr = tmpPtr;
}
else
ptr = new int[1];//数组原来是空的
ptr[size++] = v;//加入新的数组元素
}
第五节 流插入运算符和流提取运算符的重载
问题1:cout<<5<<”this”为什么能够成立?
问题2:cout是什么?<<为什么能用在cout上?
1.流插入运算符的重载
cout是在iostream中定义的ostream类的对象。之所以<<能用在cout上是因为,在iostream中对<<进行了重载。
考虑到我们要执行对5的操作也要执行对this的操作,如果我们定义的重载函数返回值为void或者int类型,都无法保证后面的两次甚至更多输出能够成立。但是如果我们将其定义为ostream类型的话,那么对5操作后,还是ostream类型,那么就可以继续对this操作了,因此要把返回值类型定义为ostream。即下面的格式:
ostream & ostream::operator<<(int n)
{
//代码
return *this;
}
ostream & ostream::operator<<(const char *s)
{
//代码
return *this;
}
cout<<5<<”this”本质上的函数调用形式是:
cout.operator<<(5).operator<<(“this”);
例1:假定下面程序输出为5hello,该补写些什么?
class CStudent{
public: int nAge;
};
int main(){
CStudent s;
s.nAge = 5;
cout << s <<“hello”;
return 0;
}
需要重载左移运算符,如下:
由于<<已经在ostream中成员函数重载,因此在这里我们只能定义为全局函数进行重载,所以需要两个参数。如下面代码所示,o其实就是对象cout。
ostream & operator<<(ostream & o, const CStudent & s){
o<<s.nAge;
return o;
}
例题2:假定c是Complex复数类对象,现在希望写”cout << c;”,就能以”a+bi”的形式输出c的值;写“cin>>c”就能从键盘接受“a+bi”形式的输入,并且使得c.real = a,c.imag = b。
int main(){
Complex c;
int n;
cin >> c >> n;
cout << c << “,” <<n;
return 0;
}
程序运行结果可以如下:
输入:13.2+133i 87
输出:13.2+133i,87
代码如下:
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r=0,double i =0):real(r),imag(i){};
friend ostream & operator<<(ostream & os, const Complex &c);
friend istream & operator >>(istream & is, Compex & c);
};
ostream & operator <<(ostream & os, const Complex & c)
{
os<<c.real<<“+”<<c.imag<<“i”;
return os;
}
istream & operator >>(istream & is, Complex & c)
{
string s;
is >> s;//将”a+bi”作为字符串读入,中间不能有空格
int pos = s.find(“+”,9);
string sTmp = s.substr(0,pos);//分离出代表实部的字符串
c.real = atof(sTmp.c_str());//atof库函数能讲const char*指针指向的内容转换成float
sTmp = s.substr(pos+1,s.length()-pos-2);//分离出代表虚部的字符串
c.imag = atof(sTmp.c_str());
return is;
}
int main(){
Complex c;
int n;
cin >> c >> n;
cout << c << “,” <<n;
return 0;
}
选择题:重载“<<”用于将自定义的对象通过cout输出时,以下说法正确的是:
C 可以将“<<”重载为全局函数,第一个参数以及返回值,类型都是ostream &。
第六节 类型转换运算符的重载
代码如下:
#include <iostream>
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r=0,double i =0):real(r),imag(i){};
operator double(){return real;}//类型转换运算符重载时不写返回值类型,因为返回值类型就是它本身
};
int main()
{
Complex c(1.2,3.4);
cout << (double)c <endl;//输出1.2
double n = 2 + c;//c被自动用类型转换运算符,等价与double n = 2+c.operator double()
cout << n;//输出3.2
}
第七节 自增自减运算符的重载
自增运算符++、自减运算符–有前置/后置之分,为了区别所重载的是前置运算符还是后置运算符,C++规定:
(1)前置运算符作为一元运算符重载:
重载为成员函数时:
T & operator++()
T & operator—()
重载为全局函数时:
T1 & operator++(T2)
T1 & operator—(T2)
(2)后置运算符作为二元运算符重载,多写一个没用的参数int:
重载为成员函数时:
T operator++(int)
T operator–(int)
重载为全局函数时:
T1 operator++(T2, int)
T1 operator–(T2, int)
但是在没有后置运算符重载而有前置运算符重载的情况下,在vs中,obj++也调用前置重载,而dev则令obj++编译出错。
例题1:
int main()
{
CDemo d(5);
cout<(d++) <<“,”;//等价于d.operator++(0);
cout << d << “,”;
cout << (++d) << “,”;//等价于d.operator++();
cout << d << endl;
cout << (d–) << “,”;//等价于d.operator–(0);
cout << d << “,”;
cout << (–d) << “,”;//等价于d.operator–();
cout << d << endl;
return 0;
}
输出结果:
5,6,7,7
7,6,5,5
如何编写CDemo?
class CDemo{
int n;
public:
CDemo(int i=0):n(i){}
CDemo & operator++();//前置形式++n返回值就是n的引用,所以这里要用引用
CDemo operator++(int);//后置形式,n++返回的是一个临时变量,所以这里不能用引用
operator int(){return n;}
friend CDemo & operator–(CDemo &);
friend CDemo operator–(CDemo &, int);
};
CDemo & CDemo::operator++():
{//前置
n ++;
return *this;
}
CDemo CDemo::operator++(int k):
{//后置
CDemo tmp(*this);//记录修改前的对象
n++;
return tmp;//返回修改前的对象
}//s++即为s.operator++(0);
CDemo & operator–(CDemo & d){//前置
d.n–;
return d;
}
CDemo operator–(CDemo &d, int){//后置
CDemo tmp(d);
d.n–;
return tmp;
}//s–即为operator–(s,0)
可以看出,前置操作因为少一个步骤,所以运算速度快于后置操作。所以提倡写++i。
运算符重载的注意事项:
1.C++不允许定义新的运算符;
2.重载后运算符的含义应该符合日常习惯;
3.运算符重载不改变运算符的优先级;
4.以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;
5.重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须生命为成员函数。