Bootstrap

C++【类与对象】——运算符重载


Note:
i.视频为 黑马程序员C++视频(121-126),系列文章为视频听课笔记;
ii.难度指数:++
iii.不论变量、函数名、标识符形式怎样复杂,只要我们考虑编程的本质是对内存的操作,对内存进行分析,一切逻辑都会变得清晰。

一、运算符重载

1.定义

对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

二、加号运算符重载

1.code格式

(1)通过成员函数实现加号运算符重载

通过成员函数重载加号,实现两个示例对象属性相加并返回拥有相加属性的新的对象。

//代码示例
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	int m_A;
	int m_B;
	//通过成员函数实现加号运算符重载
	person operator +(person &p)//定义函数类型为person,
	{
		person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;//返回拥有相加属性的新的对象
	}
};

void test01()
{
	person p1;
	p1.m_A = 10;
	p1.m_B = 20;
	person p2;
	p2.m_A = 10;
	p2.m_B = 20;
	person p3 = p1 + p2;
	cout << p3.m_A << endl;//20
	cout << p3.m_B << endl;//40
}

int main()
{
	test01();
}

(2)通过全局函数实现加号运算符重载

通过全局函数重载加号,实现两个示例对象属性相加并返回拥有相加属性的新的对象。

//代码示例
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	int m_A;
	int m_B;
};

//通过全局函数实现运算符重载
person operator + (person &p1, person &p2)
{
	person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;//返回拥有相加属性的新的对象
}
void test01()
{
	person p1;
	p1.m_A = 10;
	p1.m_B = 20;
	person p2;
	p2.m_A = 10;
	p2.m_B = 20;
	person p3 = p1 + p2;
	cout << p3.m_A << endl;
	cout << p3.m_B << endl;
}

int main()
{
	test01();
}

2.作用

实现两个自定义数据类型(非编译器内置数据类型)的相加。

3.拓展

(1)运算符重载与函数重载可以同时发生

上述加号运算符重载涉及到operator +函数,也能实现函数重载。
函数重载:函数重载是函数名相同,但传入参数类型等不同的情况下能够实现对不同函数的调用。

//代码示例
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	int m_A;
	int m_B;
};
//通过全局函数实现运算符重载
person operator + (person &p1, person &p2)
{
	person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}
//全局函数重载,与上面的函数第二个形参类型不同
person operator + (person &p1, int num)
{
	person temp;
	temp.m_A = p1.m_A + num;
	temp.m_B = p1.m_B + num;
	return temp;
}

void test01()
{
	person p1;
	p1.m_A = 10;
	p1.m_B = 20;
	person p2;
	p2.m_A = 10;
	p2.m_B = 20;
	person p3 = p1 + p2;//调用上面第一个全局函数
	cout << "p3.m_A=" << p3.m_A << endl;//20
	cout << "p3.m_B=" << p3.m_B << endl;//40
	person p4 = p1 + 5;//由于加号运算符后面的数据类型为整型,调用上面第二个全局函数实现加号运算符的重载
	cout << "p4.m_A="<< p4.m_A << endl;//15
	cout << "p4.m_B="<< p4.m_B << endl;//25
}

int main()
{
	test01();
}

该图为上述代码的输出,由输出可见,由于加号运算符前后数据类型不同,在进行加法运算时,加号运算符重载时调用的是两个不同的全局函数。
在这里插入图片描述

【一点思考】
1)重载类似于利用同一个标志符,但使其在不同情况下发挥的作用不同。例如运算符重载是指运算符相同,但数据类型不同时能够实现自定义数据类型的运算;函数重载是函数名相同,但是参数类型等不同的情况下能够实现对不同函数的调用。
2)加号运算符的重载通过定义一个名为operator +的成员函数或者全局函数来实现,当需要定义多个operator +函数实现自定义类型数据的相加运算时,也会发生函数重载的现象。

三、左移运算符重载

左移运算符即<<

1.code格式

(1)通过全局函数实现左移运算符重载

通过全局函数重载左移运算符,实现自定义数据类型——的输出。

//代码示例一
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	int m_A;
	int m_B;
};

void operator << (ostream &out,person &p)//out为cout的引用
{
	out << p.m_A << endl;
	out << p.m_B << endl;
}
void test01()
{
	person p;
	p.m_A = 10;
	p.m_B = 20;
	cout << p;//注意这里调用<<重载函数,所传参数为cout和p!
	cout << p << "hello world" << endl;//报错!
}

int main()
{
	test01();
	return 0;
}

代码示例一运行结果:
在这里插入图片描述

代码示例一能够实现<<重载,但不能实现不断重载,即cout << p <<...会报错,这是因为<<重载函数返回类型是voidvoid不能继续对重载函数进行调用。要实现对重载函数的不断调用,只要实现重载函数返回值为cout即可,具体代码见示例二。

//代码示例二
#include<iostream>
#include<string>
using namespace std;

class person
{
public:
	int m_A;
	int m_B;
};

ostream & operator << (ostream &out,person &p)
{
	out << p.m_A << endl;
	out << p.m_B << endl;
	return out;
}
void test01()
{
	person p;
	p.m_A = 10;
	p.m_B = 20;
	cout << p << "hello";
}

int main()
{
	test01();
	return 0;
}

代码示例二运行结果:
在这里插入图片描述

2.作用

可以输出自定义数据类型。

【总结】
1)左移运算符的重载通过定义一个名为operator <<的全局函数来实现;
2)这里出现了新的数据类型,即cout的数据类型:ostream
cout能作全局函数的参数也是我没想到的hiahiahia

3.拓展

思考:当上述代码示例中类的属性为私有属性时,怎么输出?
答:利用全局函数作友元,在类中加上friend+全局函数声明即可。代码见示例三:

//代码示例三
#include<iostream>
#include<string>
using namespace std;

class person
{
	friend ostream & operator << (ostream &out, person &p);
	friend void test01();
private:
	int m_A;
	int m_B;
};

ostream & operator << (ostream &out,person &p)
{
	out << p.m_A << endl;
	out << p.m_B << endl;
	return out;
}
void test01()
{
	person p;
	p.m_A = 10;
	p.m_B = 20;
	cout << p << "hello";
}

int main()
{
	test01();
	return 0;
}

四、递增运算符重载

1.code格式

(1)前置递增运算符重载

前置递增运算符的作用是:先递增,后执行表达式,具体示例见代码中main函数的前三行。
代码如下所示:

//前置递增运算符重载
#include<iostream>
#include<string>
using namespace std;

class MyInteger 
{
	friend ostream & operator << (ostream &out, MyInteger MyInt);
public:
	//利用构造函数对成员属性进行初始化,不初始化就没有示例的值输出
	MyInteger()
	{
		m_num = 0;
	}
	//通过成员函数重载前置++运算符
	MyInteger& operator ++()//必须返回引用,目的是对一个数据进行操作,否则将返回新的对象的成员属性
	{
		//先递增
		this->m_num++;
		//再将自身返回
		return *this;
	}
private:
	int m_num;
};

//左移运算符重载全局函数
ostream & operator << (ostream &out, MyInteger MyInt)
{
	cout << MyInt.m_num<< endl;
	return out;
}

//测试重载的前置递增效果
void test01()
{
	MyInteger myint;
	//若重载函数返回为非引用,输出结果为:
	cout << ++(++myint) << endl;//2
	cout << myint << endl;//1
	//若重载函数返回为引用,输出结果为:
	cout << ++(++myint) << endl;//2
	cout << myint << endl;//2
}
int main()
{
	//看一下前置递增运算符的作用,重载要实现相同的作用
	int a = 0;
	cout << ++a << endl;//1
	cout << a << endl;//1
	test01();
	return 0;
}

【关于前置递增运算符重载函数返回必须为引用的原因】: 如果重载函数返回值非引用,那么每次++myint后都会创建一个新的MyInteger对象,再进行递增就不是基于实例对象myint了,而是在每次++后创建的新的MyInteger实例对象上。

(2)后置递增运算符重载

后置递增运算符的作用是:先执行表达式,后递增。具体示例见代码中main函数前三行。
代码如下所示:

//后置运算符重载实现
#include<iostream>
#include<string>
using namespace std;

class MyInteger
{
	friend ostream & operator << (ostream &out, MyInteger myint);
public:
	MyInteger()
	{
		m_num = 0;
	}
	MyInteger operator ++(int)//不能返回引用,因为temp仅为局部变量
	{
		//先返回原先的值->先记录先前的值
		MyInteger temp = *this;
		//再递增
		this->m_num++;
		//最后返回
		return temp;
	}
private:
	int m_num;
};

ostream & operator << (ostream &out, MyInteger myint)
{
	cout << myint.m_num;
	return out;
}
void test01()
{
	MyInteger myint;
	cout << myint++ << endl;//0
	cout << myint << endl;//1
}

int main()
{
	//看一下后置递增运算符的作用,重载要实现相同的作用
	int a = 0;
	cout << a++ << endl;//0
	cout << a << endl;//1
	test01();
	return 0;
}

【关于重载函数为什么不能返回引用】:因为temp为局部变量,函数执行结束后即释放。在引用中我们提到,引用作为函数返回值时,不能返回局部变量的引用。

2.作用

通过递增运算符重载,实现自定义的整型数据递增,例如类中某整型成员属性的的递增。同理,上述代码也可进行递减运算符的重载,进而实现自定义整型数据的递减。

五、赋值运算符重载

(1)浅拷贝&深拷贝

【深浅拷贝的起源&图解&解决方法】:
【起源】:前面学到,c++在创建类时会默认分配三个函数
1)默认构造函数(无参,函数体为空)
2)默认析构函数(无参,函数体为空)
3)默认拷贝构造函数(实现对属性的值拷贝
然而,当创建的类中包含堆区指针数据的成员属性时,若调用默认拷贝构造函数(即浅拷贝)会导致堆区指针内存的重复释放(释放由析构函数实现),进而导致程序无法运行。
实际上,c++在创建类时还会默认分配赋值运算符重载函数,即operator =函数。
【动态图解】:

【解决方法】:
1)自定义拷贝构造函数,实现深拷贝
2)赋值运算符重载

下面我们将从解决浅拷贝方法的角度来介绍赋值运算符重载。

(2)自定义拷贝构造函数解决浅拷贝问题

自定义拷贝构造函数见代码中所示,其开辟了新的堆区内存储存实例对象p1中的成员属性,实现了将实例对象p中指针数据m_age指向的值赋值给p1,而不是单纯的将m_age赋值。

#include<string>
#include<iostream>
using namespace std;

class person 
{
public:
	//创建年龄属性,将其开辟到堆区
	person(int age)
	{
		cout << "构造函数调用" << endl;
		m_age = new int(age);//注意格式
	}
	//自定义拷贝构造函数,实现深拷贝
	person(const person &p)
	{
		cout << "自定义拷贝函数调用" << endl;
		m_age = new int(*(p.m_age));
	}
	~person()
	{
		cout << "析构函数调用" << endl;
		if (m_age != NULL)
		{
			delete m_age;//释放堆区内存
			m_age = NULL;
		}
	}
	int * m_age;
};

void test01()
{
	person p(18);
	person p1(p);
	cout << "p的年龄为:" << *p.m_age << endl;//18
	cout << "p1的年龄为:" << *p1.m_age << endl;//18
}
int main()
{
	test01();
	return 0;
}

上述代码执行的输出结果如下所示:
在这里插入图片描述

(3)赋值运算符重载解决浅拷贝问题

赋值运算符重载函数见代码中person & operator = (person &p)所示:

【思考】:
要学会分析定义重载函数返回值为不同类型的逻辑,如下所示代码中,若定义赋值运算符重载函数的返回值类型为person类,则执行一次重载函数,都会创建一个新的person实例对象。

#include<string>
#include<iostream>
using namespace std;

class person 
{
public:
	person(int age)
	{
		cout << "有参构造函数调用" << endl;
		//创建年龄属性,将其开辟到堆区
		m_age = new int(age);//注意格式
	}
	~person()
	{
		cout << "析构函数调用" << endl;
		if (m_age != NULL)
		{
			delete m_age;
			m_age = NULL;
		}
	}
	person & operator = (person &p)//连续赋值情况下,p3=p2=p1,首先执行p2=p1;这里定义的返回值和返回类型是p2的返回值和类型。若返回值类型为person,则创建的是一个新的person实例对象,报错!
	{
		//编译器默认浅拷贝
		//m_age = p.m_age;
		//先判断是否有p1中属性在堆区,如果有先释放干净
		if (m_age != NULL)
		{
			delete m_age;
			m_age = NULL;
		}
		//指向深拷贝
		m_age = new int(*p.m_age);
		return *this;
	}
	int * m_age;
};

void test01()
{
	person p(18);//堆区创建内存存储
	person p1(20);//堆区创建内存存储
	p1 = p;//调用operator+赋值运算符重载函数
	cout << "p的年龄为:"<<*p.m_age<<endl;
	cout << "p1的年龄为:" << *p1.m_age << endl;
}

int main()
{
	//test01();
	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
/***********************************************/
	person p1(10);
	person p2(20);
	person p3(30);
	p3 = p2 = p1;//自定义类属性连续赋值
	cout << "p1的值为" << *p1.m_age << endl;//10
	cout << "p2的值为" << *p2.m_age << endl;//10
	cout << "p3的值为" << *p3.m_age << endl;//10
	return 0;
}

六、关系运算符重载

(1)code格式

以关系运算符“==”和“!=”为例,核心函数为bool operator ==(person &p)bool operator !=(person &p)

#include<iostream>
#include<string>
using namespace std;

class person 
{
public:
	person(int age, string name)
	{
		m_age = age;
		m_name = name;
	}
	//“==”关系运算符重载
	bool operator ==(person &p)
	{
		if (this->m_age == p.m_age && this->m_name == p.m_name)
		{
			return true;
		}
		else
			return false;
	}
	//“!=”关系运算符重载
	bool operator !=(person &p)
	{
		if (this->m_age != p.m_age || this->m_name != p.m_name)
		{
			return true;
		}
		else
			return false;
	}
	int m_age;
	string m_name;
};

void test01()
{
	person p1(18, "TOM");
	person p2(25, "吴晗");
	person p3(18, "TOM");
	if (p1 == p3)
	{
		cout<<"p1和p3是相等的"<<endl;
	}
	if (p1 != p2)
	{
		cout << "p1和p2是不相等的" << endl;
	}
}

int main()
{
	test01();
	return 0;
}

(2)作用

重载任意关系运算符,实现自定义数据类型的对比。

七、函数调用运算符重载(重点)

(1)code格式

函数调用运算符即(),该部分涉及()的重载。

//运算符()重载
#include<iostream>
#include<string>
using namespace std;

//利用重载实现打印输出
class Myprint
{
public:
	void operator ()(string zfc)
	{
		cout << zfc << endl;
	}
};
//利用重载实现两数相加
class Myadd 
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};
//测试打印输出
void test01()
{
	Myprint p1;
	p1("hhhhhhhhhhhh");//使用起来非常像函数调用,因此运算符()重载函数在使用时也称为仿函数
	Myprint()("吴晗");//匿名函数对象,涉及实例对象以及重载函数,执行完后立即释放

}
//测试两数相加
void test02()
{
	Myadd shs;
	int sum1 = shs(12, 13);
	cout << "两数之和=" << sum1 << endl;//25
	int sum2 = Myadd()(19, 81);
	cout << "两数之和=" << sum2 << endl;//100
}

int main()
{
	test01();
	test02();
	return 0;
}

(2)作用

知道运算符()也可以重载,了解仿函数以及匿名函数对象,后面学习STL会经常用到。

【总结】:
1)仿函数:由于运算符()重载函数的使用与函数调用类似,因此其在使用时被称为仿函数。
2)匿名函数对象:类名+()即为一个匿名函数对象,执行完后立即释放。

八、全文总结

C++【类与对象】——运算符重载串讲,涉及:
1)加号运算符,+:实现自定义数据类型相加
2)左移运算符,<<:实现自定义数据类型输出
3)递增运算符,前置++后置++:实现自定义数据类型递增
4)关系运算符,==!=:实现自定义数据类型比较操作
5)函数调用运算符,():定义()实现自定义功能
核心函数为operator加上述运算符,难点为重载运算符的连续调用

;