C++核心编程:P8->类和对象----运算符重载

C++核心编程:P8->类和对象----运算符重载,第1张

本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下
C++核心编程:P1->程序的内存模型
C++核心编程:P2->引用
C++核心编程:P3->函数提高
C++核心编程:P4->类和对象----封装
C++核心编程:P5->类和对象----对象的初始化和清理
C++核心编程:P6->类和对象----C++对象模型和this指针
C++核心编程:P7->类和对象----友元

文章目录
  • 前言
  • 一、加号运算符重载
    • 1.1 成员函数重载+号运算符
    • 1.2 全局函数重载+号运算符
    • 1.3 运算符重载的函数重载
  • 二、左移运算符重载
    • 2.1 成员函数重载的缺陷
    • 2.2 全局函数重载
    • 2.3 一些问题
  • 三、递增运算符重载
    • 3.1 重载前置
    • 3.2 后置递增
  • 四、赋值运算符重载
    • 4.1 赋值运算符重载的一些问题
    • 4.2 解决方案
  • 五、关系运算符重载
  • 六、函数调用运算符重载


前言

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


一、加号运算符重载

对于内置的数据类型,编译器知道如何运算,比如下面我们都知道c的结果是20。

int a = 10;
int b = 10;
int c = a +  b;

如果现在我们有个Person类,类中有两个成员变量m_A和m_B。现在我们有两个Person对象p1和p2,如果我们直接通过加号运算符+将两个对象的成员变量相加并创建出一个新的对象是不行的。

class Person
{
public:
	int m_A;
	int m_B;
};
Person p1;
p1.m_A = 10;
p1.m_B = 10;
Person p2;
p2.m_A = 10;
p2.m_B = 10;
Person p3 = p1 + p2;

1.1 成员函数重载+号运算符

此时我们可以想个办法:通过自己写个成员函数,实现两个对象的成员变量相加并返回新的对象。

class Person
{
public:
	int m_A;
	int m_B;

	Person PersonAddPerson(Person& p)
	{
		Person tmp;
		tmp.m_A = this->m_A + p.m_A;
		tmp.m_B = this->m_B + p.m_B;
		return tmp;
	}
};

这种方法可行但也有缺点。如果多个人都实现了这样的函数,可能每个人的函数名都不一样,比如我写的叫PersonAddPerson,别人写的叫person_add_person…为了方便编写代码,编译器就直接给我们提供一个函数名:oprator+

Person operator+(Person& p)
{
	Person tmp;
	tmp.m_A = this->m_A + p.m_A;
	tmp.m_B = this->m_B + p.m_B;
	return tmp;
}
Person p3 = p1.operator+(p2);

当都使用编译器提供的这种名称时,就可以简化为两个数相加的形式,这就和我们预期的写法一致。以上就是通过成员函数重载+号

Person p3 = p1 + p2;

整体实现代码如下

#include 
#include 
using namespace std;

class Person {
public:
	Person() {};
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
	//成员函数实现 + 号运算符重载
	Person operator+(const Person& p) {
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}
public:
	int m_A;
	int m_B;
};

void test01()
{
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;
	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;
	//本质上是Person p3 = p1.operator+(p2)
	Person p3 = p1 + p2;
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
}

int main(void)
{
	test01();

	return 0;
}

运行,可以看出可以通过+号直接完成两个对象的成员变量相加。


1.2 全局函数重载+号运算符

我们也可以通过全局函数重载+号运算符,此时就需要两个参数。

Person operator+(Person &p1, Person &p2)
{
	Person tmp;
	tmp.m_A = p1.m_A + p2.m_A;
	tmp.m_B = p1.m_B + p2.m_B;
	return tmp;
}
Person p3 = operator+(p1, p2);

此时调用的方式可以简化为

Person p3 = p1 + p2;

完整代码如下

#include 
#include 
using namespace std;

class Person {
public:
	Person() {};
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
public:
	int m_A;
	int m_B;
};

Person operator+(Person &p1, Person &p2)
{
	Person tmp;
	tmp.m_A = p1.m_A + p2.m_A;
	tmp.m_B = p1.m_B + p2.m_B;
	return tmp;
}

void test01()
{
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;
	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;
	//本质上是Person p3 = operator+(p1, p2)
	Person p3 = p1 + p2;
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;
}

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

运行,可以看出通过+直接完成两个对象的成员变量相加


1.3 运算符重载的函数重载

如果现在我们想让Person变量和int类型变量相加,即将int类型的变量值加在Person的两个成员变量上,我们可以对运算符重载使用函数重载。

#include 
#include 
using namespace std;

class Person {
public:
	Person() {};
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
public:
	int m_A;
	int m_B;
};

Person operator+(const Person& p1, const Person& p2) {
	Person temp(0, 0);
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}

//运算符重载 可以发生函数重载 
Person operator+(const Person& p2, int val)
{
	Person temp;
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
	return temp;
}

void test() {
	Person p1(10, 10);

	Person p2 = p1 + 10; //相当于 operator+(p1,10)
	cout << "mA:" << p2.m_A << " mB:" << p2.m_B << endl;
}

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

运行,可以发现调用的是下面那个Person operator+(const Person& p2, int val)

注:

①对于内置的数据类型的表达式的的运算符是不可能改变的。
②不要滥用运算符重载。


二、左移运算符重载 2.1 成员函数重载的缺陷

我们可以通过cout + << 输出一些内置的数据类型。现在我们有个Person类,里面有m_A和m_B两个成员变量。如果想通过 << + Person对象就能直接输出m_A和m_B,则必须要重载才行。

int a = 10;
cout << a << endl; //可以输出内置数据类型
Person p;
p.m_A = 10;
p.m_B = 10;
cout << p << endl; //不行

假如利用成员函数重载左移运算符,效果如下。可以看出调用方式p.operator<<(p)不是我们想要的效果,这里出现两个对象,而用 << 输出时只有1个对象

class Person {
public:
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
	//调用方式就是p.operator<<(p)
	void operator<<(Person& p){
	}
private:
	int m_A;
	int m_B;
};

如果我们进行修改,由于成员函数重载的本质是对象去调用成员函数,这样又会使得cout在右侧

class Person {
public:
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
	//调用方式就是p.operator<<(cout)
	void operator<<(cout){
	}
private:
	int m_A;
	int m_B;
};

因此我们通常不会使用成员函数重载<<运算符,因为无法实现cout在左侧,只能利用全局函数重载左移运算符。


2.2 全局函数重载

因此我们通过全局函数来重载左移运算符,大概框架如下

class Person {
public:
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
private:
	int m_A;
	int m_B;
};

//全局函数实现左移重载
//这样就能实现 cout << p
void operator<<(cout, p) {

}

我们首先看看cout的定义,可以看出cout的数据类型是ostream,而ostream就是标准输出流类。

于是我们将ostream拿过来,而cout这个对象只能有1个,于是我们传引用

#include 
#include 
using namespace std;

class Person {
public:
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
public:
	int m_A;
	int m_B;
};

//全局函数实现左移重载
//ostream对象只能有一个
void operator<<(ostream& out, Person& p) {
	out << "a:" << p.m_A << " b:" << p.m_B;
}

void test() {
	Person p1(10, 20);
	cout << p1;
}

int main() {
	test();
	system("pause");
	return 0;
}

运行,可以看出正确重载了<<并输出了p的成员变量。


2.3 一些问题

可以看出最后没有换行。如果我们再加个 << endl,则会报错

这是因为我们在一行使用多个<<这种链式编程思想时。而我们这里返回的是void,即没有返回,所以无法追加<<。因此我们需要以引用的方式将返回cout。

通常我们在定义一个类是里面的一些成员变量权限为私有,所以可以让这个全局的重载函数作为类的友元。

#include 
#include 
using namespace std;

class Person {
	friend ostream& operator<<(ostream& out, Person& p);
public:
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

private:
	int m_A;
	int m_B;
};

//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {
	out << "a:" << p.m_A << " b:" << p.m_B;
	return out;
}

void test() {
	Person p1(10, 20);
	cout << p1 << "hello world" << endl; //链式编程
}

int main() {
	test();
	system("pause");
	return 0;
}

运行,可以看到重载函数也能直接访问私有成员变量。


三、递增运算符重载

我们先来看看前置递增和后置递增的使用

#include 
using namespace std;

int main()
{
	int a = 10;
	cout << ++a << endl; //11
	cout << a << endl;   //11

	int b = 10; 
	cout << b++ << endl; //10
	cout << b << endl;   //11

	return 0;
}

可以看到前置递增是先将变量+1然后做其它 *** 作。后置递增则是先做 *** 作然后再+1。

现在我们想自己定义一个数据类型然后实现递增运算。比如这里我们自己定义一个数据类型MyInter,含有一个成员变量m_Num。默认构造函数给其一个初始值1,然后我们希望可以重载递增运算符实现递增 *** 作。

#include 
using namespace std;

class MyInter
{
public:
	MyInter()
	{
		m_Num = 0;
	}
private:
	int m_Num;
};

MyInter myint;
cout << myint << endl; //0
cout << ++myint << endl; //1
cout << myint++ << endl; //1
cout << myint << endl; //2

如果要可以搭配使用cout,则需要先使用全局函数来重载左移运算符。

#include 
using namespace std;

class MyInter
{
	friend ostream& operator<<(ostream &cout, MyInter myint);
public:
	MyInter()
	{
		m_Num = 0;
	}
private:
	int m_Num;
};

ostream& operator<<(ostream &cout, MyInter myint)
{
	cout << myint.m_Num;
	return cout;
}

int main()
{
	MyInter myint;
	cout << myint << endl; //0

	return 0;
}

此时可以输出自己定义的数据类型了


3.1 重载前置

首先我们实现前置递增

#include 
using namespace std;

class MyInter
{
	friend ostream& operator<<(ostream &cout, MyInter myint);
public:
	MyInter()
	{
		m_Num = 0;
	}
	MyInter& operator++() //要返回引用
	{
		m_Num++; //先进行++运算
		return *this;  //再将自身返回
	}

private:
	int m_Num;
};

ostream& operator<<(ostream &cout, MyInter myint)
{
	cout << myint.m_Num;
	return cout;
}

int main()
{
	MyInter myint;
	cout << myint << endl; //0
	cout << ++myint << endl;

	return 0;
}

运行,可以看到结果正确。


这里的重载函数如果返回值会出现一些问题。对于普通数据类型,我做连续两次递增 *** 作,可以看到是对同一个变量连续做两次 *** 作,结果是正确的

int a = 0;
cout << ++(++a) << endl;  //2
cout << a << endl;  //2

如果是返回值,我们测试看一下

#include 
using namespace std;

class MyInter
{
	friend ostream& operator<<(ostream &cout, MyInter myint);
public:
	MyInter()
	{
		m_Num = 0;
	}
	MyInter operator++()
	{
		m_Num++; //先进性++运算
		return *this;  //再将自身返回
	}

private:
	int m_Num;
};

ostream& operator<<(ostream &cout, MyInter myint)
{
	cout << myint.m_Num;
	return cout;
}

int main()
{
	MyInter myint;
	cout << ++(++myint) << endl; //2
	cout << myint << endl; //1

	return 0;
}

可以看到返回值时连续做两次递增,只成功了一次,这是因为每一次返回的都是一个新的变量,下一次 *** 作是对这个新的变量做 *** 作。所以我们要返回引用,为了一直对一个数据做 *** 作。


3.2 后置递增

要实现后置递增,则需要对递增运算符重载实现函数重载。于是我们在参数里面加个int,代表一个站位参数,编译器就会认为这是后置递增。

后置递增对应的重载函数要返回值。如果返回引用就是返回一个局部对象的引用,局部对象会在函数结束后被释放。

#include 
using namespace std;

class MyInter
{
	friend ostream& operator<<(ostream &cout, MyInter myint);
public:
	MyInter()
	{
		m_Num = 0;
	}
	MyInter& operator++()
	{
		m_Num++; //先进性++运算
		return *this;  //再将自身返回
	}
	MyInter operator++(int)
	{
		//先记录当时结果
		MyInter temp = *this;
		//后递增
		m_Num++;
		//最后将记录结果做返回
		return temp;
	}
private:
	int m_Num;
};

ostream& operator<<(ostream &cout, MyInter myint)
{
	cout << myint.m_Num;
	return cout;
}

int main()
{
	MyInter myint;
	cout << myint++ << endl; //0
	cout << myint << endl; //1

	return 0;
}

运行,结果正确


四、赋值运算符重载

c++编译器会至少给一个类添加4个函数

默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值拷贝
赋值运算符 operator=, 对属性进行值拷贝


4.1 赋值运算符重载的一些问题

我们先来测试下编译器自动提供的赋值运算符。我们创建一个Person类,包含一个成员变量m_Age,是个指向开辟在堆区数据age的指针。

#include 
using namespace std;

class Person
{
public:
	Person(int age)
	{
		//将年龄数据开辟到堆区
		m_Age = new int(age);
	}
	//年龄的指针
	int *m_Age;
};

void test01()
{
	Person p1(18);
	Person p2(20);
	p2 = p1;

	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
}

int main() {
	test01();
	system("pause");
	return 0;
}

我们直接使用=,可以发现能够实现成员变量值得复制,没有错误。

由于类中有属性指向堆区,所以我们需要写个析构函数来释放这块内存

#include 
using namespace std;

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;
};

void test01()
{
	Person p1(18);
	Person p2(20);
	p2 = p1;

	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
}

int main() {
	test01();
	system("pause");
	return 0;
}

可以发现报错,这是由于出现了前面讲的深浅拷贝问题,即堆区数据重复释放。


4.2 解决方案

所以我们重载=号时要进行深拷贝,而不是直接将值进行复制。

#include 
using namespace std;

class Person
{
public:
	Person(int age)
	{
		//将年龄数据开辟到堆区
		m_Age = new int(age);
	}
	~Person()
	{
		if (m_Age != NULL)
		{
			delete m_Age;
			m_Age = NULL;
		}
	}
	void operator=(Person &p)
	{
		if (m_Age != NULL) //如果自己开辟的内存释放掉
		{
			delete m_Age;
			m_Age = NULL;
		}
		m_Age = new int(*p.m_Age); //重新开辟一块内存
	}
	//年龄的指针
	int *m_Age;
};

void test01()
{
	Person p1(18);
	Person p2(20);
	p2 = p1;

	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
}

int main() {
	test01();
	system("pause");
	return 0;
}

运行,可以看出不会出现浅拷贝了

但是还没完。对于普通类型,如果有这种连等,则是把最右边那个数赋值给左边的所有数。

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

所以需要对重载函数进行修改。注意,不要返回值,否则又会调用拷贝构造函数创建一个副本

#include 
using namespace std;

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;
};

void test01()
{
	Person p1(18);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1;

	cout << "p1的年龄为:" << *p1.m_Age << endl;
	cout << "p2的年龄为:" << *p2.m_Age << endl;
	cout << "p3的年龄为:" << *p3.m_Age << endl;
}

int main() {
	test01();
	system("pause");
	return 0;

}

运行,可以看出成功实现了连等。


五、关系运算符重载

关系运算符包含==和!=。如果现在我们想对比两个自定义的数据类型,则需要重载关系运算符。假如现在我有个Person类,包含一个string类型的成员变量name和int类型的成员变量age。如果两个Person对象的name和age相等就打印相等,否则打印不相等。

#include 
#include 
using namespace std;

class Person
{
public:
	bool operator==(Person &p)
	{
		if (this->age == p.age && this->name == p.name)
			return true;
		else
			return false;


	}
	bool operator!=(Person &p)
	{
		if (this->age != p.age || this->name != p.name)
			return true;
		else
			return false;	
	}
public:
	Person(string m_Name, int m_Age)
	{
		name = m_Name;
		age = m_Age;
	}
public:
	string name;
	int age;
};

int main()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);
	if (p1 == p2)
	{
		cout << "p1 和 p2 是相等的" << endl;
	}
	if (p1 != p2)
	{
		cout << "p1 和 p2 是不相等的" << endl;
	}

	return 0;
}

运行,可以看到结果正确。


六、函数调用运算符重载
  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活

例: 假设现在我们要创建一个MyPrint类,通过重载函数调用运算符完成字符串打印输出。

#include 
#include 
using namespace std;

class MyPrint
{
public:
	void operator()(string str)
	{
		cout << str << endl;
	}
};

int main()
{
	MyPrint myfunc;
	myfunc("hello world");

	return 0;
}

运行,可以看出类似于函数一样完成了字符串的打印输出。


例: 假设现在要创建一个MyAdd类,通过重载函数调用运算符完成两个整数相加。

#include 
#include 
using namespace std;

class MyAdd
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};

int main()
{
	MyAdd func2;
	cout << func2(10, 20) << endl;
	cout << MyAdd()(10, 20) << endl; //匿名对象调用  

	return 0;
}

运行,可以看到结果正确。其中,我们这里使用了匿名函数调用,即先通过MyAdd()创建一个匿名对象,这个匿名对象在当前行执行结束后会被释放,然后为这个匿名对象调用了重载的()运算符函数。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1295787.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存