目录
运算符重载概念
运算符重载语法
运算符重载的两种方法
运算符重载的步骤
运算符重载限制
运算符重载原则
重载输出运算符
如何判断返回引用还是普通变量?
赋值运算符重载
重载自增运算符
重载数组下标运算符[ ]
重载函数调用运算符( )
不要重载逻辑运算符
实现字符串类
智能指针->
总结
上节我们学习了多态,本节开始学习运算符重载!
运算符重载概念
所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能。因此,一个函数名就可以用来代表不同功能的函数,也就是”一名多用”。
运算符也可以重载。实际上,我们已经在不知不觉之中使用了运算符重载。例如,大家都已习惯于用加法运算符”+”对整数、单精度数和双精度数进行加法运算,如5+8, 5.8 +3.67等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的, 但由于C++已经对运算符”+”进行了重载,所以就能适用于int, float, double类型的运算。
又如”<<“是C++的位运算中的位移运算符(左移),但在输出操作中又是与流对 象cout 配合使用的流插入运算符,”>>“也是位移运算符(右移),但在输入操作中又是与流对象 cin 配合使用的流提取运算符。这就是运算符重载(operator overloading)。
直接上代码讲解
既然C++可以运算符重载,那是不是随便一个运算符我们自己都能对它重载呢?
答案是可以的,但是需要一些操作才能实现。
我们以复数的相加为例
从数学的角度来看,两个复数是允许相加的,实部与实部相加,虚部与虚部相加,但是这段代码编译之后出错了。
c1和c2都是复数Complex这个类的对象,为什么不支持相加?
首先“+”两边只支持整型,如果写成double或者float,编译器会自己转换类型,但是这里结构体加结构体就不行了,因为它缺少相加的规则,不知道是用c1里面的m_b和c2里面的m_b相加还是用c1里面的m_a和c2里面的m_b相加,编译器不会乱来。
因此我们必须指定规则(即运算符重载),重载加号运算符,让它支持两个结构体,也就是Complex类相加。
运算符重载的本质就是函数重载,我们可以将这个加号重载成成员函数,也可以重载成全局函数。
我们把它重载成成员函数:
但是C++觉得直接这样写一个+号不太好,为了区分普通的+号,所以就j加了一个关键字operator,operator和+号之间有没有空格都可以
如果想要输出的话不能直接这样写:
我们要得自己写一个输出的函数
但是每次这样写比较麻烦:
可以进一步优化一下。
凡是能支持重载的这些运算符,必须要支持它原本用法。
原本的用法是这样的
于是我们可以将这个:
简写成这样:
这样的写法就比较人性化一点了。
现在尝试将这个成员函数转换成全局函数
改写:
全局函数重载+号也可以叫做友元函数重载+号
运算符重载语法
运算符函数是一种特殊的成员函数或者友元函数;
成员函数的语法如下:
类型 类名::operator op(参数表)
一个运算符被重载后,原有的意义没有失去,只是定义了相对特定类的一个新运算符。
运算符既能被重载成成员函数,也能被重载成全局函数。
运算符重载的两种方法
1、运算符可以被重载成成员函数或者友元函数(全局函数);
2、关键区别在于成员函数具有this指针,友元函数没有this指针;
3、不管是成员函数还是友元函数,运算符的使用方法是相同的,只是传递参数的方式不同,实现代码不同,应用场合也不同。
运算符重载的步骤
1、运算符重载就是函数的重载,先写出函数名,例如:operator+()
2、根据操作数,写出函数参数;
3、根据业务,完善函数返回值、业务逻辑。
运算符重载限制
可以重载的运算符
+ - * / % ^ & | ~ ! = < > +=
-= *= /= %= ^= &= |= << >> >>= <<= == !=
>= <= && || ++ -- ->* -> [] () new delete new[] delete[]
不能重载的运算符
. :: .* ?: sizeof
运算符重载原则
不改变运算符的优先级;
不改变运算符的结合性;
不改变运算符需要的操作数;
不能创建新的运算符。
接下来重重载一些常用的运算符
重载输出运算符
之前我们说这段代码中的如果这样直接输出是错的,然后自己写了个show()函数输出。
如果我们非得这样写,我们就得重载<<这个运算符
以全局函数的形式重载<<
注:cout<<后面可以接很多东西,比如cout<<d<<endl表示先把d输出到cout,然后再把endl输出到cout,如果再继续加cout<<d<<endl<<c<<b...那就是把后面的东西全输出到cout,这种几乎可以无限添加的情况我们叫做链式编程。
因为cout<<c的意义是将c输出到cout这个对象里面(cout是ostream这个类的对象),所以cout<<c的返回值应该是一个ostream类型。
接下来我们先来分析这段代码
这段代码编译后,test()=1;这句代码会报错,因为这里test()这个函数不能作为左值,执行了int test(){}这个函数后返回的值一个整数,是一个常量。
比如int test(){return 100;}返回的是100,那test()=1;这句就是100=1; 常量不能被赋值。
结论:返回引用的函数可以作为左值。返回非引用的函数不可以作为左值。
以后我们需要一个函数作为左值的话,那么它的返回值必须是引用,如果不需要它作为左值的话,返回就可以不是引用。
那我们来研究一下之前写的+号的重载函数的返回值要不要返回引用?
那就再试一下这段代码中我们这样写到底行不行,我们就能判断出+号的返回值到底是常量还是变量。
结果是不行的
那也就是说+号不能作为左值,也就是它的返回值是个常量
那么我们就能确定我们重载+号的这个函数也应该是返回常量,不能作为左值的,因此我们就只能写成这样了:
现在回头看看我们这段代码中的cout<<c
cout<<c作为<<endl的左值,那么它应该是一个变量,既然是变量,那我们函数的返回值也就必须是返回引用才行,所以这里改成这样:
否则它返回的就是常量,如果是常量的话我们后面就没法将endl写入到cout里面了。
这样我们就可以直接写成cout<<c<<endl;,不用写另外写一个show,然后通过对象c访问show()也能打印了。
我们再考虑一个问题,能不能把这个全局函数改成成员函数?
结果是不行的
当编译器看到主函数中的cout<<c;这句话的时候,它就翻译成cout.operator<<(c); 那cout是一个对象,调用operator<<这个函数,传的参数是c,但是我们是在Complex这个类里面重载的,也就是说operator<<这个函数是在Complex这个类里面的,就算是访问也是通过Complex这个对象访问才是。
cout是ostream这个类的对象,在ostream里面的确有重载<<,但是并没有重载成operator<<的参数可以是Complex类型的,Complex这个类型是我们自己的定义的,编译器编译的时候不知道我们会用到Complex这个类。
如果我们写成这样的话编译是没问题的:
但是这就是不算是运算符重载了,因为运算符重载的原则是一定要支持运算符原来的写法。
结论:输出运算符只能重载成全局函数(友元函数),不能重载成成员函数(输入运算符同理)。因为左操作数不能修改(比如说cout是ostream这个类的对象,ostream是库里面的类,不能被修改,以后我们遇到这种左操作数不能被修改的就不能重载成成员函数),如果左操作数是我们自定义的,就可以重载成成员函数。
再来研究一个问题,能不能直接写成这样?
结果是不能
这句代码会被编译器翻译成operator<<(cout, c1+c2), c1+c2的+号返回值是个常量,而我们写的重载<<函数的形参是个引用类型的
我们之前就说过不能用常量初始化普通引用。
但是可以用常量初始化常引用,所以我们这里可以加上const,让它变成常引用(常引用也可以被变量初始化,所以加上const更加合适)
记得把这里的声明也改成一致的
总结:
输入输出运算符
重载输入输出运算符
ostream &operator<<(ostream &out, Complex &c);
istream &operator>>(istream &in, Complex &c);
如何判断返回引用还是普通变量?
1、看该运算符能不能作为左值使用,如果可以,则返回引用;
2、输入输出运算符支持链式编程,函数返回值充当左值,需要返回引用。
3、当无法修改左操作数时,使用全局函数进行重载。
4、=, [], ()和->操作符只能通过成员函数进行重载。
赋值运算符重载
1、赋值运算符必须被重载成成员函数。每个类都默认提供了赋值运算符重载。
2、重载赋值运算符,必须使用深拷贝。
虽然所有的类都默认提供赋值运算符重载,那我们不用写它的重载函数也行,但是有时候还是需要写一下。
直接上代码讲解
在这段代码中,a2可以直接赋值给a1吗?
结果是可以的,编译通过
结论:在C语言中相同结构体是可以相互赋值的。
在C++里面我们可以理解成所有类都默认重载了赋值运算符。
但是如果我们这里写一个析构函数的话程序就出问题了
原本两个类在内存中的布局是这样的
现在a2赋值给a1,来个data都指向了0x200
然后main运行结束,调用析构函数释放内存,首先a1析构函数释放了0x200,然后a2再调用析构函数释放0x200这块内存的时候就出问题了,因为刚刚0x200被释放过了,同一块内存只能释放一次,所以程序就报错了。
所以凡是涉及到地址的都需要进行深拷贝(这个我们之前也讲过了,忘记的自己去翻之前的博客)。
深拷贝我们就要重写赋值运算符
我们先来用这段代码来测试一下赋值运算符能不能作为左值
结果是可以的,也就是说赋值运算符的返回值是个变量,那么我们写赋值运算符的重载函数时返回值应该为引用类型。
接下来我们就要实现深拷贝
当a2赋值给a1的时候,要先把原本a1的data指向的那块内存释放掉
然后将a2的size赋值给a1,a1根据size=10申请一块内存,然后将a2中的data指向的那块内存0x200里面的东西拷贝到刚刚申请的新的内存中。
这样编译就没有问题了。
总结:
1、赋值运算符必须被重载成成员函数。每个类都默认提供了赋值运算符重载。
2、重载赋值运算符,必须使用深拷贝。
重载自增运算符
如何区分前置++和后置++?
占位参数
直接上代码讲解:
目前这段代码还不支持c1++,因为目前编译器还不知道对象怎么自增操作
自增运算符可以重载成成员函数也可以重载成全局函数
我们写成成员函数
自增运算符有前置++和后置++
从函数外形来看不好区分前置和后置++
我们可以先用这段代码来测试一下自增运算符的返回值是变量还是常量,再确定重载函数的返回值是不是需要返回引用。
编译显示m++=1;这句有问题
也就是说m++的返回值不能作为左值,那么可以断定后置++的返回值是常量,那重载后置++的函数的返回值应该是常量,前置++的返回值是变量,那重载前置++的函数的返回值应该是引用类型。
这样就能区别前置++和后置++
那么问题又来了,这两个函数可以构成函数重载吗?
我们说过返回值不能作为函数重载的标准,所以这两个函数不能构成重载的关系。所以编译是会报错的,他们现在其实是同一个函数,只是返回值不一样,属于重复定义函数,不是重载的关系。
怎么办?
我们可以在后置++的括号中放一个占位参数。
这样就能把两个函数区分开了。
注意:如果这里的const去掉的话会报错,因为后置++返回值是常量,常量不能赋值给普通引用
重载数组下标运算符[ ]
我们在访问一个数组元素的时候经常中括号[ ]来访问,如a[0]
在什么情况下需要重载数组下标运算符[ ]?
比如说这样一个场景
现在这句代码是有问题的,因为a1是个对象,而不是数组。
如果这个a1是个数组对象,如果我们特别想这样访问数组里面的对象的话,你就需要重载数组下标运算符[ ]
直接上代码讲解:
注意:数组下标运算符只能重载成成员函数。
像a1[0]=100;能赋值,那么就代码[ ]可以作为左值,所以返回值应该是个引用,在这段代码中数组元素是这个整型
以后有类似这种地址操作的对象可以考虑重载下标运算符,可以通过对象和下标加[ ]来访问更加方便
重载函数调用运算符( )
我们调用函数的时候要用到的小括号,比如调用函数a1传实参为100,那就是a1(100),这个小括号就是函数调用运算符。
直接上代码讲解:
但是我们说过运算符被重载后,它应该是支持以前的写法的。
那么按理来说也可以写成这样:
编译和运行都没问题,但是在没有上下文的情况下,我们可能就直接把这个prt当成是函数名
所以没有上下文的情况下,prt就是函数(但是注意它其实是一个对象)。我们可以直接将这种对象称为函数对象。
如何实现一个函数对象?
来一个类,在这个类里面重载函数调用运算符,创建这个类的对象,这个对象就可以成为函数对象。
它的意义类似于C语言里面的回调函数,可以修改一个函数的功能。
它的使用场景之后学到STL的时候会详细讲。
小结:
[ ]和()运算符只能重载成成员函数,不能使用友元函数。
重载函数调用运算符(),创建的对象叫做函数对象。
不要重载逻辑运算符
1、&&和||是C++中非常特殊的操作符
2、&&和||内置实现了短路规则
3、操作符重载是靠函数重载来完成的
4、操作数作为函数参数传递
5、C++的函数参数都会被求值,无法实现短路规则
直接上代码:
先看这段代码的输出结果是什么?
结果输出的是111
逻辑与要求左右两边的条件都成立才可以,也就是这里的逻辑与肯定是不成立的,以为f1返回的是0
f1()没成立,由于逻辑运算符有“短路原则”,所以后面的f2()没有执行
那接下来尝试重载逻辑运算符
目前来看好像&&是可以被重载的,但是请再看如果把这里改成这样:
按理来说c1实部和虚部都为0,那么c1不成立,按照逻辑运算符的“短路原则”,c1不成立那么if就不会再去判断&&后面的c1+c2了,也就是说程序根本不会打印这句话
但是结果它打印出来了
也就是说它违背了我们逻辑运算符的“短路原则”,那这个重载逻辑运算符的操作就有问题。
编译器会把这句话翻译成c1.operator&&(c1+c2),然后c1+c2又翻译成c1.operator+(c2);
总体就是c1.operator&&(c1.operator+(c2)),也就是说它要先把函数的参数的结果算出来,执行顺序就是先计算c2,然后计算c1.operator+(c2)这句,因此不满足“短路原则”。
结论就是:逻辑运算符可以重载,但是违背了短路原则,所以不要重载逻辑运算符。
实现字符串类
C++专门用于操作字符串的类--string(要包含头文件<string>)
实现mystring
string这个类里面重载了输出运算符。
代码演示:
String这个类里面还重装载了下标运算符、+号运算符、赋值运算符
基本上我们能想到的一些操作,在string这个类里面基本都实现了,直接用就可以了。
接下来我们来实现一个mystring的类,在这个类里面我们实现输出和输入运算符、下标运算符,+号运算符,赋值运算符,+=运算符......
完整代码:
#include <iostream>
using namespace std;
class mystring
{
private:
char *data;
int size;
public:
mystring();
mystring(int,char);//函数的声明可以先不用写函数名,只写类型
mystring(const char*);
mystring(const mystring&);
~mystring();
//输入和输出运算符只能重载成全局函数(友元函数)
friend ostream &operator<<(ostream &out, const mystring &s);
friend istream &operator>>(istream &in,mystring &s);//从in输入到s,所以不能const
char &operator[](int);
mystring &operator+=(const char*);
mystring operator+(const char*);
mystring operator+(const mystring &);
mystring &operator=(const mystring &);
bool operator>(const mystring&);
bool operator==(const mystring &);
};
mystring::mystring()
{
data=NULL;
size=0;
}
mystring::mystring(const char*s)//传一个字符串过来
{
this->size=strlen(s);
data=new char[size+1];//strlen不计算\0,所以要+1
strcpy(data,s);//strcpy回拷贝\0,不用管
}
mystring::mystring(int num,char ch)//传num个字符过来
{
this->size=num;
data=new char[size+1];//给\0预留空间
memset(data,ch,size);//从data这个地址开始填充,填充为ch,填充size个字节,最后一个是\0
data[size]='\0';
}
//拷贝构造函数,深拷贝
mystring::mystring(const mystring &s)//传一个对象,将这个对象赋值给this这个对象
{
this->size=s.size;
this->data=new char[this->size+1];
strcpy(this->data,s.data);
}
mystring::~mystring()
{
if(this->data)
delete data;
data=NULL;
}
//输出运算符重载(全局)
ostream &operator<<(ostream &out,const mystring &s)//可以将一个对象输出到cout
{
out<<s.data;//将data里面的值输出到out
return out;
}
//输入运算符重载(全局)
istream &operator>>(istream &in,mystring &s)//从标准输入中获取数据到s中的data
{
char buf[1024]={0};
cin>>buf;
s.size=strlen(buf);
s.data=new char[s.size+1];//给\0预留一个空间
strcpy(s.data,buf);
return in;
}
//数组下标运算符重载,返回该下标对应的元素
char &mystring::operator[](int idx)
{
return this->data[idx];
}
//+=重载,对象+=字符串
mystring &mystring::operator+=(const char*s)
{
char *buf=new char[this->size+strlen(s)];//先申请一块新的内存
strcpy(buf,this->data);//将data和s拷贝进去
strcat(buf,s);
if(this->data)//如果不为空
delete[] data;//释放this->data
this->data=buf;//让data重新指向buf的位置
this->size=this->size+strlen(s);
return *this;
}
//+重载,对象=对象+字符串
mystring mystring::operator+(const char*s)
{
mystring str;
str.size=this->size+strlen(s);
str.data=new char[str.size+1];
strcpy(str.data,this->data);
strcat(str.data,s);
return str;
}
//+重载,对象=对象+对象
mystring mystring::operator+(const mystring &s)
{
mystring str;
str.size=this->size+s.size;
str.data=new char[str.size+1];
strcpy(str.data,this->data);
strcat(str.data,s.data);
return str;
}
//=重载,对象=对象
mystring &mystring::operator=(const mystring &s)
{
if(*this==s)//比如如果是s1=s1这种情况就直接返回,但是不支持:对象==对象这种写法,所以我们需要重载==
return *this;
if(this->data)
delete[] this->data;
this->size=s.size;
this->data=new char[this->size+1];
strcpy(this->data,s.data);
return *this;
}
//>重载,对象>对象
bool mystring::operator>(const mystring &s)
{
if(strcmp(this->data,s.data)>0)
return true;
else
return false;
}
//==重载,对象==对象
bool mystring::operator==(const mystring &s)
{
if(this->size==s.size&&this->data==s.data)//如果是同一个对象比如s1==s1
return true;
if(this->size==s.size&&strcmp(this->data,s.data)==0)//如果是不同对象,但是data的值相等比如s1=s2,两个都是保存的helleworld
return true;
return false;
}
int main()
{
mystring s1; //无参构造函数
mystring s2(10,'a');//有参构造函数
mystring s3("hello");
mystring s4=s3;//拷贝构造函数
cin>>s1; //从标准输入获取
cout<<s1<<endl;
cout<<s2<<endl;
cout<<s3<<endl;
cout<<s4<<endl;
cout<<s1[0]<<endl;//如果我们输入的是hello,那么s1[0]就是h
s1+="12345";
cout<<s1<<endl;
s1=s1+"678";
cout<<s1<<endl;
s1=s1+s2;
cout<<s1<<endl;
s1=s2;
cout<<s1<<endl;
if(s1>s3)
{
cout<<"s1>s3"<<endl;
}
else
{
cout<<"s1<s3"<<endl;
}
/*mystring s1("helloworld");
s1=s1;
cout<<s1<<endl;*/
return 0;
}
运行结果:
补充命令34:底行模式”:sp 文件名”
在某个文件的当前界面下,输入冒号底行模式,然后输入sp 文件名,回车后就可以在同一个界面下打开另一个文件,然后就可以用yy和p从一个文件复制和粘贴过来到另一个文件中。
补充命令35:ctrl+ww可以在同一个界面下的两个文件之间切换光标。
补充命令36:如果用以上两条命令完成了两个文件之间的内容复制粘贴操作后,用wq分别保存好后,就可以按shift+zz分别关闭两个文件。
智能指针->
动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
为了更加容易(更加安全)的使用动态内存,C++引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
c++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr。包含头文件memory
auto_ptr<Test> my_memory(new Test);
my_memory->func(); //类似指针的功能
直接上代码讲解
以下这段代码是有问题的,因为创建对象的时候申请了内存但是并没有调用析构函数释放内存。
如果在大型的项目中,如果没有手动delete释放内存的话,内存会变得越来越少,因此我们需要有一种机制内自动释放内存。
这里就用上了智能指针:
注意这里编译后如果出现以下提示则表示当前编译器不支持智能指针
智能指针是C++11才有的内容,2011年的C++才有,红帽5 (Red Hat)还是09年左右的产物。
建议从这里开始可能就要换成Ubuntu进行学习,后面安装数据库,做项目,都要用Ubuntu。
在虚拟机上可以用Ubuntu和Red Hat,用哪个版本取决于我们做项目的开发板,如果是10240就用Red Hat(32位),如果开发板是树莓派就用Ubuntu(64位)
换成Ubuntu编译我们上面的智能指针之后就通过了,可以看到它的确自动调用了析构函数
注:编译智能指针的时候要加上-std=c++11
注意:智能指针不允许这样写
如果非要这样操作的话,我们可以这样写
此时再通过up1访问show的话就出问题了,因为不能有两个智能指针同时指向Student的对象
这里的unique_ptr可以直接换成 auto_ptr
总结
操作符重载是C++的强大特性之一;
操作符重载的本质是通过函数扩展操作符的语义,操作符重载遵循函数重载的规则;
operator关键字是操作符重载的关键;
操作符重载可以使用成员函数或者友元函数实现;
不能重载的运算符 . :: .* ?: sizeof
=, [], ()和->操作符只能通过成员函数进行重载
++操作符通过一个int参数进行前置与后置的重载
当不能修改左操作数时,只能重载成全局函数(输入输出)
所有类都默认重载了赋值运算符,但是都是浅拷贝
C++中不要重载&&和||操作符
下节开始学习模板!
如有问题可评论区或者私信留言,如果想要进扣扣交流群请私信!