我们知道重载是编译器允许两者重名,有了符号重载,我们就可以自定义符号
运算符重载的实质:将运算符重载为一个函数,使用运算符就是对重载函数的调用。
注意事项
1、运算符可以重载为全局函数,此时函数的参数个数 = 运算符的操作数个数
2、运算符可以重载为成员函数,此时函数的参数个数 = 运算符的操作数个数 - 1
(运算符的操作数有一个要作为对象,调用该成员函数)
3、运算符重载不改变运算符的优先级
4、有些运算符不能被重载: . .* :: ?: sizeof
5、运算符 = () [ ] -> 只能重载为成员函数,不能重载为全局函数
语法: operator + 符号
其实就是写一个函数,operator + 符号 是函数名,函数体里写想要实现的操作
例如: void operator+(int a) { ... } //重载了+号运算符
加号运算符重载(+)
我们先定义一个Person类,里面有2个成员变量,现在我们创建两个对象,再令它们相加,目的是令它们的成员变量各自相加,可以吗?不可以,如下:
class Person{
public:
Person(){ //初始化数据
num_1 = 1;
num_2 = 2;
}
int num_1;
int num_2;
};
Person p1, p2;
Person p = p1 + p2;
/*我们的目的是让:
p.num_1 = p1.num_1 + p2.num_1
p.num_2 = p1.num_2 + p2.num_2
显然这不行
*/
所以我们可以重载一下加号运算符,就可以实现上述操作:
1、全局函数重载
下面这段代码我们定义一个全局函数func,它可以实现传入两个对象,在内部将它们的成员各自相加,然后再返回一个Person类的类型。
Person func(Person& p1, Person& p2){
Person temp;
temp.num_1 = p1.num_1 + p2.num_1;
temp.num_2 = p1.num_2 + p2.num_2;
return temp;
}
然后我们只要把函数名 func 换成 operator+ 就大功告成了,如下:
Person operator+(Person& p1, Person& p2){
Person temp;
temp.num_1 = p1.num_1 + p2.num_1;
temp.num_2 = p1.num_2 + p2.num_2;
return temp;
}
这样编译器就已经重载好了+运算符,为了让大家更明白一点,我们先像平时一样调用这个函数,然后再写简化后的版本:
Person p1, p2;
//老实调用函数:
Person p = operator+(p1, p2);
cout << p.num_1 << p.num_2 << endl; //输出 2 4
//简化后直接操作:
Person p = p1 + p2;
cout << p.num_1 << p.num_2 << endl; //输出 2 4
2、成员函数重载
成员函数重载的话,调用该函数的时候,就需要用一个对象来调用,那么传入的参数就变成一个对象了(全局函数是两个),不理解的可以看看代码思考一下:
class Person {
public:
Person() { //初始化属性值
num_1 = 1;
num_2 = 2;
}
//成员函数重载
Person operator+(Person& p2) {
Person temp;
temp.num_1 = this->num_1 + p2.num_1; //this指针指向调用的对象
temp.num_2 = this->num_2 + p2.num_2; //相当于是上面的p1
return temp;
}
//属性
int num_1;
int num_2;
};
Person p1, p2;
//老实调用函数:
Person p = p1.operator+(p2);
//简化后:
Person p = p1 + p2;
我们既然能重载+号,也同样可以重载 - * / % ,只要在operator后面加上相应的符号即可
左移运算符重载(<<)
同样以上面的Person类举例,原先 cout << 只能输出一个数据,要想输出多个就要写多个数据,现在我们重载它,让它能够实现,输入一个对象,输出对象的所以属性:
class Person{
public:
Person(){
num_1 = 1;
num_2 = 2;
}
int num_1;
int num_2;
};
Person p;
cout << p; //企图输出 p.num_1 和 p.num_2
注意!cout 是输出流类ostream实例化出的一个对象,它的数据类型是ostream,同理 cin 是istream,感兴趣的可以去了解一下
1、全局函数重载
注意 cout 要写在前面,后面会讲原因
void operator<<(ostream& cout, Person& p){
cout << p.num_1 << p.num_2 << endl;
}
Person p;
//老实调用函数:
operator<<(cout, p); //输出 1 2
//简化:
cout << p; //输出 1 2
但是上面的函数返回的是void,那么就不能一直输出,比如 cout << p << endl 就不行,只能到 cout << p,后面不能再加东西。因为输出的必须是输出流类型,才能一直输出下去,所以我们可以让函数返回一个输出流类型,这样就可以一直输出了。
//改进后的代码,函数返回输出流类型
ostream& operator<<(ostream& cout, Person& p){
cout << p.num_1 << p.num_2 << endl;
return cout;
}
Person p;
cout << p << endl; //输出 1 2 换行
2、成员函数重载
我们会发现一个问题:如果用成员函数重载<< ,那么需要一个对象来调用这个函数,原先全局函数的参数有两个:cout 和 p,现在就只剩一个了:cout ,因为对象p需要写在函数外面来调用这个函数,此时就会出现上面没说明的问题:cout 一定要写在前面
下面说明为什么:如果cout写在后面,也是可以的,但是想要输出p,我们就要这么写:
p << cout,两者的顺序要反一下,同理,用成员函数重载的话,cout就一定在p后面了
class Person {
public:
Person(){
num_1 = 1;
num_2 = 2;
}
ostream& operator<<(ostream& cout){
cout << num_1 << num_2 << endl;
return cout;
}
int num_1;
int num_2;
};
Person p;
//老实调用函数:
p.operator<<(cout); //从这里可以看出 cout 一定只能在 p 的后面
//简化:
p << cout << endl;
tips:不要用成员函数重载左移运算符
递增运算符重载(++)
tips:最好写在类内(方便),即成员函数
我们知道++a的操作,会让a + 1,现在我们想实现,让a每次都加10,并且只能自己定义一个整型数据,只能在成员函数内重载++运算符(--也是一样)
1、前置++
class Person {
public:
//初始化
Person() {
a = 1;
}
//重载++运算符
void operator++() {
a += 10;
}
//属性
int a;
};
//重载<<运算符,方便输出对象
ostream& operator<<(ostream& cout, Person& p) {
cout << p.a;
return cout;
}
Person p;
//老实调用函数:(正确的)
p.operator++();
cout << p << endl; //输出 11
//简化:(其实不能这么写,会报错)
cout << ++p << endl; //错误的
我们发现最后一行简化版的代码是错误的,为什么呢?
解释:
最后一行代码中的 << 是重载后的 <<,它要传入的参数是 Person 类,所以 cout << p 是可行的,但是 cout << ++p不行,我们看重载++运算符的成员函数,它返回的是一个void,而不是Person类,所以我们把代码修改成下面这样就可以了:
class Person {
public:
//初始化
Person() {
a = 1;
}
//重载++运算符
Person& operator++() { //此处最好返回引用,不要返回值
a += 10;
return *this; //*this返回自身
}
//属性
int a;
};
//重载<<运算符,方便输出对象
ostream& operator<<(ostream& cout, Person& p) {
cout << p.a;
return cout;
}
Person p;
//简化:
cout << ++p << endl; //正确
2、后置++
我们要知道 a++ 与 ++a 的区别,a++ 先调用a再+1,++a先+1再调用a
int a = 1;
cout << a++ << a; //先输出 1 再输出 2
int a = 1;
cout << ++a << a; //都输出 2
有几点要注意:
1、上面的前置++函数返回的是自身的引用,因为如果返回值,当我们多次调用该函数的时候就会发现数据丢失,因为返回值的操作实际上编译器会拷贝一个副本然后返回。但是下面的后置++,就需要返回值了,因为最好不要返回局部变量的引用,具体看代码
2、如果一个程序同时重载了前置++和后置++,那么这两个函数就会重定义(因为名字一模一样),为了区分它们,就需要在其中一个函数内加一个参数int,int是占位参数,在调用的时候不用传入实参进去,它的目的是做区分,让两个函数形成重载。
class Person {
public:
//初始化
Person(){
a = 1;
}
//前置++
Person& operator++() {
a += 10;
return *this;
}
//后置++
Person operator++(int) { //中间要加 int 做区分
Person temp = *this; //先记录一下当前数据,后面要输出
a += 10;
return temp;
}
//属性
int a;
};
//!!后置++函数返回的是值,所以重载<<运算符的时候,形参也应该是值,而不是引用
ostream& operator<<(ostream& cout, Person p) { //本来是 Person& p
cout << p.a;
return cout;
}
//输出,简化版:
cout << p++ << endl; //输出 1
cout << p << endl; //输出 10
赋值运算符重载(=)
只能成员函数重载
首先要知道:
当我们创建了一个类时,编译器会自动为我们添加四个函数,1、默认构造函数 2、默认析构函数 3、默认拷贝构造函数 4、赋值运算符重载(浅拷贝)函数
既然编译器都已经重载了赋值运算符了,为什么我们还要再写一遍呢?因为当我们在类中开辟了堆区的内存时,我们需要在析构函数中将其释放,而编译器自带的函数实现的是浅拷贝的操作,这时就会出现堆区内存重复释放的问题,所以我们要重新用深拷贝的方式写一遍。详细可见我的这篇博客:C++拷贝构造函数详解(浅拷贝、深拷贝)_错过王小姐的博客-CSDN博客
因为编译器自动重载了 赋值运算符,所以我们可以实现以下操作:
class Person {
public:
//有参构造
Person(int age) {
m_age = age;
}
//属性
int m_age;
};
Person p1(18);
Person p2(22);
p2 = p1; //可以直接写 p2 = p1,不会报错
cout << p2.m_age << endl;
如果我们要在堆区开辟内存,下面的代码就会出错:
class Person {
public:
//有参构造
Person(int age) {
m_age = new int(age); //在堆区开辟内存
}
//析构函数(释放堆区内存)
~Person() {
if(m_age != NULL) {
delete m_age;
m_age = NULL;
}
}
//属性
int* m_age;
};
Person p1(18);
Person p2(22);
p2 = p1; //可以直接写 p2 = p1,不会报错
cout << *p2.m_age << endl;
错误原因:堆区的内存被重复释放。 解决办法:重载赋值运算符,使用深拷贝
单看重载赋值运算符的函数:
//系统给的浅拷贝
void operator=(Person& p) {
m_age = p.m_age;
}
//我们要改成深拷贝
void operator=(Person& p) {
m_age = new int(*p.m_age);
}
但是这样写还会有2个问题:
1、Person p1(18);
Person p2(22);
p2 = p1;
可以看到 p2 本身有一块堆区的内存,里面存放了数据 22,但是现在我们又重新给它开辟了一块内存,里面存放数据 18,这样原先的那块内存就无法释放了,所以要先释放原先的内存。
2、不能实现 连等于 的操作:p1 = p2 = p3
因为上面的函数返回值是void ,使用连等于相当于是 p1 = void,会报错,所以我们要让函数返回自身的数据类型(return *this 即返回自身)
//修改后的代码
Person& operator=(Person& p) {
//释放原先在堆区开辟的内存
if(m_age != NULL) {
delete m_age;
m_age = NULL;
}
//重新指向一块新的内存
m_age = new int(*p.m_age);
//返回自身
return *this;
}
看完整代码:
class Person {
public:
//有参构造
Person(int age) {
m_age = new int(age); //在堆区开辟内存
}
//析构函数(释放堆区内存)
~Person() {
if(m_age != NULL) {
delete m_age;
m_age = NULL;
}
}
//赋值运算符重载
Person& operator=(Person& p) {
if(m_age != NULL) {
delete m_age;
m_age = NULL;
}
m_age = new int(*p.m_age);
return *this;
}
//属性
int* m_age;
};
Person p1(18);
Person p2(22);
p2 = p1;
cout << *p2.m_age << endl;
关系运算符重载(==)
关系运算符有:== ,!= , < , > , <=, >=
想要实现的操作:判断两个对象是否相等
全局函数重载
class Person {
public:
//有参构造
Person(string name, int age) {
m_name = name;
m_age = age;
}
//属性
string m_name;
int m_age;
};
//重载==运算符
bool operator==(Person& p1, Person& p2) {
if (p1.m_name == p2.m_name && p1.m_age == p2.m_age) {
return true;
} else {
return false;
}
}
Person p1("Bob", 18);
Person p2("Bob", 18);
if(p1 == p2){ //此时就可以直接比较两个对象
cout << "same" << endl;
}
成员函数重载
class Person {
public:
//有参构造
Person(string name, int age) {
m_name = name;
m_age = age;
}
//重载==运算符
bool operator==(Person& p) {
if(m_name == p.m_name && m_age == p.m_age) {
return true;
} else {
return false;
}
}
//属性
string m_name;
int m_age;
};
Person p1("Bob", 18);
Person p2("Bob", 18);
if(p1 == p2){ //此时就可以直接比较两个对象
cout << "same" << endl;
}
函数调用运算符重载()
只能成员函数重载
就是重载括号(),由于重载后使用的方式非常像函数的调用,所以称为仿函数
仿函数非常灵活,没有固定的写法,主要体现在它的参数个数不受限制,自己想写几个就写几个,但是对于其他的运算符,参数基本上是2个,比如 + ,==,它们的操作是两者相加或者两者相等,所以不需要第三个参数。
可以理解为:在类内写函数,之后创建的对象本身就是函数
class Person {
public:
//打印字符串的函数
void operator()(string str) {
cout << str << endl;
}
//计算三者平均值的函数
float operator()(int a, int b, int c) {
return (a + b + c) / 3;
}
};
Person p;
//调用打印字符串
p("hello world"); //p是不是很像函数名,就像调用函数一样
//调用三者平均值
float n = p(1, 2, 3);
cout << n << endl;