C++类和对象中运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
场景描述:对于内置的数据类型,例如整型、浮点型等,编译器都知道如何进行运算。但是对于一些自定义的数据类型,编译器不知道如何进行运算。
需要解决的问题:因此需要我们对一些运算符进行重载,赋予这些运算符另种功能实现对自定义数据类型的运算。以下主要展示加号、左移、递增、赋值、关系以及函数调用运算符的重载。
加号运算符重载
作用:实现两个自定义数据类型相加的运算
场景描述:对于内置的数据类型,编译器都知道如何进行运算,例如:
int a = 10;
int b = 10;
int c = a + b;//编译器可以算出c是多少
但是,对于如下场景,例如
class Person
{
public:
Person(int a, int b):mA(a), mB(b)
{ }
int mA;
int mB;
};
void test1()
{
Person p1(10, 10);
Person p2(20, 20);
//Person p3 = p1 + p2;
}
如果我想实现,“Person p3 = p1 + p2;”,也就是p3.mA=p2.mA+p1.mA,p3.mB=p2.mB+p1.mB。此时需要重新定义加号运算符,即实现加号运算符重载。解决办法:自己写函数来实现。
思路过程写在图片里
![](https://img-blog.csdnimg.cn/img_convert/1249884b4171298a7117b8d2e54617ce.png)
![](https://img-blog.csdnimg.cn/img_convert/9aa26d774c6c63646e3e14bcf609b642.png)
总结起来就是编译器起了通用的名字“operator+”,实现重载+,可以通过成员函数实现,也可以通过全局函数实现。
operator+
自己写的成员函数通过引用的方式传递,返回本身,需要在函数调用前创建一个新对象。对比老师写的,老师通过值传递调用拷贝构造函数做值拷贝,返回在成员函数创建的新对象。这个地方函数名是在讲到operator+出现之前写的,没有改名字。
//Person类的成员函数——自己写的,我的是返回本身
Person& PersonAddP(Person& p1, Person& p2)
{
this->mA = p1.mA + p2.mA;
this->mB = p1.mB + p2.mB;
return *this;
}
//老师教例展示的——老师通过值传递调用拷贝函数返回一个新的对象temp
Person PersonAddPerson(Person& p)
{
Person temp;
temp.mA = this->mA + p.mA;
temp.mB = this->mB + p.mB;
return temp;
}
//自己写的测试
void test1()
{
Person p1(10, 10);
Person p2(20, 20);
Person p3(0, 0);
p3.PersonAddP(p1, p2);
cout << "p3.mA = " << p3.mA << "\tp3.mB = " << p3.mB << endl;
}
![](https://img-blog.csdnimg.cn/img_convert/099eacd068722f67bb74ee162f7b2421.png)
上面展示的例子可以看出,不同的人取不同名字的成员函数。那么为了优化,编译器起了一个通用的成员函数名叫“operator+”,这个成员函数用来实现新数据类型的数据可以进行加号运算。
整体实现
#include <iostream>
using namespace std;
class Person
{
public:
//1.成员函数重载+
/*Person operator+(Person& p)
{
Person temp;
temp.mA = p.mA + this->mA;
temp.mB = p.mB + this->mB;
return temp;
}*/
int mA;
int mB;
};
//2.全局函数
Person operator+(Person p1, Person p2)
{
Person temp;
temp.mA = p1.mA + p2.mA;
temp.mB = p1.mB + p2.mB;
return temp;
}
//3.运算符重载 也可以发生函数重载
Person operator+(Person& p, int num)
{
Person temp;
temp.mA = p.mA + num;
temp.mB = p.mB + num;
return temp;
}
//重载+
void test2()
{
Person p1;
p1.mA = 10;
p1.mB = 10;
Person p2;
p2.mA = 20;
p2.mB = 20;
//1.成员函数重载本质的调用
//Person p3 = p1.operator+(p2);//Person p3 = p1 + p2;是简化
//2.全局函数重载本质的调用
//Person p3 = operator+(p1, p2);//Person p3 = p1 + p2;是简化
Person p3 = p1 + p2;
//3.运算符重载 也可以发生函数重载
Person p4 = p1 + 30;
cout << "全局函数重载+" << endl;
cout << "p3.mA = " << p3.mA << "\tp3.mB = " << p3.mB << endl;
cout << "运算符重载也可以发生函数重载" << endl << endl;
cout << "p4.mA = " << p4.mA << "\tp3.mB = " << p4.mB << endl;
}
int main()
{
test2();//函数重载+
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/7ba7cc39311f043611f71f4cbf03459d.png)
总结
1.对于内置数据类型的表达式的运算符是不可以改变的;
2.成员函数实现重载+,写在类内,传入一个对象;
3.全局函数实现重载+,写在类外,传入两个对象;
4.运算符重载也可以函数重载,和全局函数写法类似;
5.不要滥用运算符重载,比如operator+里边写成了两个自定义数据的相减
左移运算符重载
作用:可以输出自定义数据类型
场景描述
对于对象p在输出时,常规的左移运算符,需要加上“.m_A”或者“.m_B”才能正常输出。但是直接输出p,编译器不知道怎么输出p的属性。
int a = 10;
cout << a << endl;//编译器正常输出
Person p;
p.m_A = 10;
p.m_B = 10;
//cout << p << endl;//能否直接这样做输出?
使用全局函数,不用成员函数实现左移运算符重载的原因
不知道函数的返回值是什么的时候,先写void类型;
如果函数传入对象的话,在使用的时候还要传入一个对象,就变成p.operator<<(p),和想要的结结果不一样,所以不应该传入对象;
如果传入“cout”,就变成p.operator<<(cout),简化p << cout,这种方式可以写,但是预期效果是cout << p。使用成员函数无法达到预期效果,所以使用全局函数。
综上,不会利用成员函数重载<<运算符,因为无法实现<<在左侧。所以使用全局函数来实现。
实现过程及思路
不知道函数类型的返回值是什么的时候,先写void类型;
其次要把cout放在左边,p在右边,才有预期效果
要知道cout是什么类型,选中cout--右键 转到定义--看到cout类型是ostream。o指输出,stream指流,cout是属于标准的输出流类。此外,这个对象cout只能有一个,不能创建新对象,否则后面没法用,采用引用的方式传递。
![](https://img-blog.csdnimg.cn/img_convert/bf87fe1b96e7ff1a3e4cc882973a853c.png)
//只能利用全局函数重载<<运算符
void operator<<(ostream& cout, Person& p)//operator<<(cout, p) 简化 cout << p
{
cout << " mA = " << p.mA << " mB = " << p.mB;
}
![](https://img-blog.csdnimg.cn/img_convert/bef853a8eecb34b165fbaa05b041f4ad.png)
注意点:如果想追加换行“endl”,会提示报错。在之前的学习中,原本的cout可以无限追加是因为使用了链式思想来编程,也就是可以一直左移。在重载左移的全局函数中,函数类型是void,没有返回值。
如果,也想用追加,那么需要函数在调用后再次返回cout,才能无限追加,因此如下修改
//只能利用全局函数重载<<运算符
ostream& operator<<(ostream& cout, Person& p)//operator<<(cout, p) 简化 cout << p
{
cout << " mA = " << p.mA << " mB = " << p.mB;
return cout;
}
之前使用的cout可以无限追加,其本质也是一样,调用cout输出函数的时候返回了cout。
全局函数在中形参cout也可以换成别的名字,因为这是引用的调用方式,引用的本意就是起别名,别名和原名都是指向同一块内存空间。此时把cout换成out或者其他都是可以的,并没有改变本质。例如:
//只能利用全局函数重载<<运算符
ostream& operator<<(ostream& output, Person& p)//operator<<(cout, p) 简化 cout << p
{
output << " mA = " << p.mA << " mB = " << p.mB;
return output;
}
当对象p的属性是私有权限内容时,比如想访问mC属性时,可以通过把重载函数做类的友元来访问。
class Person
{
friend ostream& operator<<(ostream& cout, Person& p);
public:
int mA;
int mB;
private:
int mC;
};
整体实现
#include <iostream>
using namespace std;
//不成员函数重载<<运算符原因
/*
不会成员函数重载<<运算符,因为无法实现<<在左侧。如果成员函数传入“cout”,最后效果就变成p.operator<<(cout),简化p << cout,
这种方式可以写,但是预期效果是cout << p。使用成员函数无法达到预期效果,所以使用全局函数
*/
class Person
{
friend ostream& operator<<(ostream& cout, Person& p);
public:
int mA;
int mB;
private:
int mC;
};
//只能利用全局函数重载<<运算符
ostream& operator<<(ostream& cout, Person& p)//operator<<(cout, p) 简化 cout << p
{
cout << " mA = " << p.mA << " mB = " << p.mB;
return cout;//cout只是一个别名,也可以换成其他名称output等,和原名指向同一块空间
}
void test1()
{
Person p1;
p1.mA = 56;
p1.mB = 45;
//cout << "p1.mA = " << p1.mA << endl;//编译器正常输出
cout << "p1 = " << p1 << endl;//无法输出,编译器不知道输出p1的哪个属性,需要重载<<
}
int main()
{
test1();
system("pause");
return 0;
}
总结
重载想要实现的功能,直接输出对象;
只能使用全局函数的原因;
cout数据类型是输出流ostream,并且采用引用的方式传递,形参名字可以更改;
要想无限输出,重载函数应该返回cout,其类型也应是输出流ostream;
访问类的私有权限内容时,让重载函数作为类的友元
递增运算符重载
作用: 通过重载递增运算符,实现自己的整型数据
预期效果
自定义整型数据类,实现递增功能
MyInteger myint;
cout << myint << endl;//初始值是0
cout << ++myint << endl;//1,先+1再输出
cout << myint++ << endl;//1,先输出再+1
cout << myint << endl;//2
思路
![](https://img-blog.csdnimg.cn/img_convert/7b0f657f8e84e00ef2247080aa73fa20.png)
代码有四个需要实现的模块
自定义整型类型数据模块
//自定义整型
class MyInteger
{
//要让重载函数访问类的私有权限内容
friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
MyInteger()
{
m_Num = 0;
}
private:
int m_Num;
};
重载<<
想正常输出的话,内置cout是不认识自定义的整型类型数据的。所以需要重载<<
//重载<<
ostream& operator<<(ostream& cout, MyInteger myint)
{
cout << "myint = " << myint.m_Num;
return cout;
}
前置递增
//自定义整型
class MyInteger
{
friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
MyInteger()
{
m_Num = 0;
}
//重载前置++运算符
MyInteger& operator++()//递增使用时直接使用++符号,因此不需要传参
{
m_Num++;//先进行++运算
return *this;//再返回自身
}
private:
int m_Num;
};
前置递增注意点
重载递增函数应该返回的是一个整型类型的数据,即自定义的整型数据类型MyInteger,否则cout不能识别做了递增运算的变量是什么类型的数据。因此,重载递增函数的类型是自定义的整型数据类型MyInteger。
此外,重载递增函数应该返回自身,因此使用this指针。
重载递增函数使用引用的方式传递的原因:
![](https://img-blog.csdnimg.cn/img_convert/d30dcbc2622bf33e2395261dc1ff9da6.png)
首先,看内置前置递增是怎么实现的。上面的例子可以看出来,无论a做多少次++运算,都是对a本身进行做递增运算,并没有创建新的对象。如果不使用引用传递,采用值传递的话,最后递增重载函数返回的是创建的新对象a',而不是a本身。因此,采用引用传递,对同一块内存空间进行操作。
值传递示例
![](https://img-blog.csdnimg.cn/img_convert/d0f93263e7a807e4cdc9e7ba883c01cc.png)
后置递增
#include <iostream>
using namespace std;
//自定义整型
class MyInteger
{
friend ostream& operator<<(ostream& cout, MyInteger myint);
public:
MyInteger()
{
m_Num = 0;
}
//重载前置++运算符 返回引用
MyInteger& operator++()//递增使用时直接使用++符号,因此不需要传参
{
m_Num++;//先进行++运算
return *this;//再返回自身
}
//重载后置++运算符 返回值
MyInteger operator++(int)//int代表占位参数,用于区分前置和后置递增
{
MyInteger temp = *this;//先记录
m_Num++;//再进行++运算
return temp;//最后返回结果
}
private:
int m_Num;
};
//重载<<
ostream& operator<<(ostream& cout, MyInteger myint)
{
cout << "myint = " << myint.m_Num;
return cout;
}
void test1()
{
MyInteger myint1;
cout << "前置递增重载" << endl;
cout << myint1 << endl;//因为myint是自定义的MyInteger类型的整型数据,cout不知道如何输出。需要重载<<
cout << ++myint1 << endl;
MyInteger myint2;
cout << "后置递增重载" << endl;
cout << myint2 << endl;//因为myint是自定义的MyInteger类型的整型数据,cout不知道如何输出。需要重载<<
cout << myint2++ << endl;
}
int main()
{
test1();
/*int a = 0;
cout << "++a:" << ++a << endl;
cout << "++(++a):" << ++(++a) << endl;*/
system("pause");
return 0;
}
后置递增注意点
占位参数区分前置递增和后置递增
在不知道返回数据类型是什么时,使用void,但此时后置递增函数和前置的发生了重定义。是因为此时两个函数的作用域相同(都在类内),函数名相同,参数也一样(都没有)。要明确重定义是同一块作用域、函数名相同,参数相同,函数类型是不能作为重定义的区分。因此在后置递增重载函数的形参传入占位参数int
![](https://img-blog.csdnimg.cn/img_convert/4fa1c847f4ac2f525b86dab948e65b42.png)
先记录当前结果,再递增,最后返回结果
如果直接先返回值,那么后面的递增操作都不会再进行。因此,需要先记录当前结果,再递增。
和前置递增重载一样,后置递增重载函数类型应该是MyInteger
和前置递增重载不同的是,后置递增重载函数是值传递,返回值,不能是引用传递。前置递增重载重载函数是引用传递,返回引用,不能是值传递。
![](https://img-blog.csdnimg.cn/img_convert/70d1b3121d33486d1e78e94a6ad48c7c.png)
如果是引用传递,这个局部对象temp在第一句,红框代码执行完之后就会被释放。再返回引用就是非法操作了。如果是值传递,这个局部对象temp在第一句,红框代码执行完之后没有被释放,之后的++运算和返回都是合法的。
总结
占位参数区分前置递增重载和后置递增重载
前置递增重载返回引用,后置递增重载返回值
自定义整型数据类
赋值运算符重载
c++编译器至少给一个类添加4个函数
1. 默认构造函数(无参,函数体为空)
2. 默认析构函数(无参,函数体为空)
3. 默认拷贝构造函数,对属性进行值拷贝,会引发深浅拷贝的问题,使用深拷贝解决浅拷贝。可以回顾之前的学习笔记深拷贝和浅拷贝
4. 赋值运算符 operator=, 对属性进行值拷贝,也会引发深浅拷贝的问题
深浅拷贝的问题
也就是说,只要是涉及到值拷贝,都会引发深浅拷贝的问题。如果类中有属性指向堆区,也就是有属性存放在堆区,那么就会出现堆区重复释放的问题,即浅拷贝,因此做赋值操作时也会出现深浅拷贝问题。还是要使用深拷贝来解决。
场景描述,解决浅拷贝重复释放的问题
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
mAge = new int(age);//age传进来,然后通过new开辟到堆区存放,并且通过mAge指针来维护
}
int* mAge;//把数据开辟到堆区
};
void test1()
{
Person p1(25);
Person p2(24);
p2 = p1;//赋值操作
cout << "p1的年龄是" << *p1.mAge << endl;
cout << "p2的年龄是" << *p2.mAge << endl;
}
int main()
{
test1();
system("pause");
return 0;
}
目前为止,p1、p2都可以正常输出,并且p1的值也可以赋值给p2。但是p1、p2都是堆区的数据,堆区的数据需要程序员手动释放。需要在成员函数Person函数后添加析构函数。
~Person()
{
if (mAge != NULL)
{
delete mAge;
mAge = NULL;
}
}
此时,程序会崩掉,出现异常断点。原因如下
![](https://img-blog.csdnimg.cn/img_convert/588f0bbed950fdb6d60db81c93ff1bd0.png)
存放在堆区的p1记录的是一个地址,这个地址指向的内容是18。
当写了代码,p2 = p1时,相当于把p1的数据逐字节地值拷贝给p2。
此时,p1和p2指向的都是同一块内存空间。当写了析构的代码之后,p2执行完之后,析构判断一下p2是否为空指针,此时不是空指针,需要释放。
接着p1也需要经过相同的过程进行释放。但是之前这块内存已经被释放了,再次释放就导致程序异常,也就是出现了浅拷贝的问题,重复释放。
因此,需要用深拷贝来解决这个问题。也就是重新开辟一块内存,让p2指向这块内存,并且内存存放的内容和p1指向的内存内容一致。
要实现上面的功能,就不能再用内置的赋值运算符了,需要重载赋值运算符。
整体实现
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
mAge = new int(age);//age传进来,然后通过new开辟到堆区存放,并且通过mAge指针来维护
}
~Person()
{
if (mAge != NULL)
{
delete mAge;
mAge = NULL;
}
}
//重载
Person& operator=(Person& p)
{
//编译器实现浅拷贝的本质:mAge = p.mAge; 不能做简单的浅拷贝
//如果有属性在堆区,首先应该释放干净,即p2已经是堆区的数据,首先需要把p2释放干净
if (mAge != NULL)
{
delete mAge;
mAge = NULL;
}
//深拷贝:new开辟一块新的内存空间,这个空间大小就是p->mAge的大小,
//所以通过解引用获得内存大小,再通过mAge来接收
mAge = new int(*p.mAge);
//返回自身
return *this;
}
int* mAge;//把数据开辟到堆区
};
void test1()
{
Person p1(25);
Person p2(24);
Person p3(30);
//p2 = p1;//赋值操作
p3 = p2 = p1;
cout << "p1的年龄是" << *p1.mAge << endl;
cout << "p2的年龄是" << *p2.mAge << endl;
cout << "p3的年龄是" << *p3.mAge << endl;
}
int main()
{
test1();
//int a = 10;
//int b = 20;
//int c = 30;
//c = b = a;
//cout << "a = " << a << endl;//10
//cout << "b = " << b << endl;//10
//cout << "c = " << c << endl;//10
system("pause");
return 0;
}
注意点
编译器实现浅拷贝的本质是“mAge = p.mAge; ”,为了避免浅拷贝,不能做简单的值拷贝;
如果有属性在堆区,首先应该释放干净。即p2已经是堆区的数据,首先需要把p2释放干净。
内置赋值运算符是一个可以连等的。所以在赋值运算符重载函数里边,为了能够实现连等操作,也就是能够让p2 = p1执行完后返回自身,这时才能实现连等操作。因此赋值运算符重载函数的类型应该是Person,并且使用引用的方式。不能返回值,前面也有类似的例子解释过了。
![](https://img-blog.csdnimg.cn/img_convert/f1b9e2604e7b1226b2261c1180e3723c.png)
总结
为什么要重载赋值运算符?编译器提供的赋值运算符是一个浅拷贝的操作,当有些成员属性创建在堆区,比如指针。这时就不能使用内置赋值运算符了,需要重载赋值运算符,否则会带来浅拷贝的重复释放的问题。
重载时,首先要判断堆区指针有没有释放干净。
如果释放干净了,再做深拷贝,新开辟一块内存,存放相同的内容。
为了实现连等的操作,使用重载函数使用引用的方式传递,返回自身。
关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
场景描述
对于内置的数据类型,编译器知道进行关系比较(相等和不等),例如:
int a = 25;
int b = 24;
if (a == b)
{
cout << "a和b是相等的" << endl;
}
但对于自定义的数据类型,编译器就不知道进行关系比较了,如下,此时需要进行关系运算符的重载。
Person p1;
Person p2;
if (p1 == p2)
{
cout << "p1和p2是相等的" << endl;
}
整体实现
#include <iostream>
using namespace std;
class Person
{
friend ostream& operator<<(ostream& cout, Person& p);//全局函数做友元
public:
Person(string name, int age)
{
mName = name;
mAge = age;
}
//重载==号
bool operator==(Person& p)
{
if (this->mName == p.mName && this->mAge == p.mAge)
{
return true;
}
return false;
}
//重载!=号
bool operator!=(Person& p)
{
if (this->mName == p.mName && this->mAge == p.mAge)
{
return false;
}
return true;
}
private:
string mName;
int mAge;
};
//重载<< 注意 引用传递 ostream数据流类型
ostream& operator<<(ostream& cout, Person& p)
{
cout << "姓名:" << p.mName;
cout << "年龄:" << p.mAge;
return cout;
}
void test1()
{
Person p1("Judge", 24);
Person p2("Bella", 25);
Person p3("Bella", 25);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl << endl;
if (p2 == p3)
{
cout << "等号重载函数:\tp3和p3是相等的" << endl << endl;
}
else
{
cout << "等号重载函数:\tp2和p3是不相等的" << endl << endl;
}
if (p2 != p1)
{
cout << "不等号重载函数:p2和p1是相等的" << endl << endl;
}
else
{
cout << "不等号重载函数:p2和p1是不相等的" << endl << endl;
}
}
int main()
{
test1();
system("pause");
return 0;
}
效果
![](https://img-blog.csdnimg.cn/img_convert/cb1849d6d80a2d72fdaf82f8384da100.png)
注意点
在判断相等和不等时,相等就是真,返回true,不相等就是假,返回false,所以重载函数的类型是布尔类型bool;
为了便于一次性输出类对象,我加了左移重载函数。重载左移时,只能用全局函数来实现以及使用引用传递,并且这个函数要在类中做友元,还要注意左移符号的数据类型。
总结
对比自定义数据类型时,需要重载==和!=。
重载函数是bool类型,因为判断==和!=,只需要知道结果是真还是假,也就是返回值是true还是false。
函数调用运算符重载
函数调用运算符
函数调用运算符: (),这个符号也可以重载。
仿函数:由于重载后的函数调用运算符(),其使用的方式非常像函数的调用,因此称为仿函数。
仿函数没有固定写法,非常灵活。在STL中使用比较多。
仿函数——例1:打印输出
#include <iostream>
using namespace std;
class MyPrint
{
public:
//重载函数调用运算符()
void operator()(string test)
{
cout << test << endl;
}
};
void MyPrint2(string test)
{
cout << test << endl;
}
void test1()
{
MyPrint myprint;
cout << "重载函数调用" << endl;
myprint("Bella is studying.");//由于使用方式与函数调用非常类似,称其为仿函数
cout << "\n函数调用" << endl;
MyPrint2("Bella is studying.");
}
int main()
{
test1();
system("pause");
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/f98b4f61c445c545732176d2003f8f8d.png)
MyPrint myprint()仿函数中,把实参传给形参test,再进行打印输出。
仿函数没有固定写法——例2:加法类
class MyAdd
{
public:
int operator()(int num1, int num2)
{
int num = num1 + num2;
return num;
}
};
void test2()
{
int a = 20;
int b = 10;
MyAdd myadd;
cout << "a = " << a << "\tb = " << b << "\na + b = " << myadd(a, b) << endl;
}
![](https://img-blog.csdnimg.cn/img_convert/086cecc1c2ac380b4bf6ff70eeb296e1.png)
可以看到第一个例子的返回类型是void,第二个例子的返回类型是int,仿函数没有固定写法,仿函数依据程序员需求来编写。
匿名函数对象
![](https://img-blog.csdnimg.cn/img_convert/c7dff56124699d3882d6bae5369c261e.png)
MyAdd()就是匿名函数对象,通过一个类型+(),编译器会创建一个匿名对象。匿名对象的特点就是在当前行执行完之后,就会立即被释放。因为实现100+100之后,我们不需要这个对象了,所以可以创建匿名对象。看到匿名函数对象,就应该想到这是给匿名对象,并且这个函数重载了()。
总结
仿函数,重载();
仿函数没有固定写法;
仿函数使用方法,可以创建变量名接收仿函数的返回值;也可以不创建变量名接收仿函数的返回值,即创建匿名对象;
匿名函数对象,形式:类型+();特点:当前行执行完之后立即被释放。
终于学完运算符重载了~~~