一文搞懂C++运算符重载(满满干货)

文章目录

前言

一、什么是运算符重载?

二、运算符重载的相关语法及注意事项

1.加号运算符重载

(Ⅰ)、成员函数实现加号运算符重载

(Ⅱ)、全局函数实现加号运算符重载

(Ⅲ)、对加号运算符重载的总结

2.左移运算符重载

3.赋值运算符重载 

 4.递减/递增运算符的重载

(Ⅰ)前置递增运算符的重载

(Ⅱ)后置递增运算符的重载 

 (Ⅲ)区别及注意事项

5.关系运算符重载 

6.函数调用运算符的重载 

总结


前言

         在C++中,运算符重载是指为自定义类型(类或结构体)定义或修改运算符的行为,使得这些运算符可以用于自定义类型的对象。运算符重载使得用户定义的类型可以像内置类型一样使用运算符,从而提高了代码的可读性和可维护性。本文将会详细介绍各类运算符重载的相关语法及常见错误。

一、什么是运算符重载?

        “什么是运算符重载?”比如,我们可以在C++中实现两个整数的相加,如:

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

        但是,我们知道与C面向过程编程不同,类和对象是C++的核心特性之一。但是如何实现两个对象(比如A对象与B对象)之间的相加呢?在默认情况下,C++编译器是识别不到两个对象之间的相加的。系统提示:没有与这些操作数匹配的“+”运算符。如:

         这样,在C++面向对象编程的过程中,造成了很大的不利。然而C++允许你对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。这就是运算符重载。但需要注意的是,对于内置数据类型表达式的运算符是不可更改的。

        下面,我将带大家详细介绍下,有关运算符重载的常见类型及注意事项。

二、运算符重载的相关语法及注意事项

1.加号运算符重载

        如下,我定义了一个Person类,我想要在test01函数中实现两个对象p1p2的相加,默认情况下,系统会提示我们“没有与这些操作数匹配的“+”运算符”。这是因为C++语法并没有定义两个对象的相加。

class Person
{
public:
	Person(int a)
	{
		m_a = a;
	}
	int m_a;
};

void test01()
{
	Person p1(10);
	Person p2(10);
	Person p3(0);
	p3 = p1 + p2;
	cout << p3.m_a << endl;
}

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

         为了解决这个问题,我们可以自己定义一个“+”运算符,来实现两个对象的相加,那么我们应该怎么实现加法运算符重载呢?C++给了我们两种方式实现运算符重载(全局函数实现重载与成员函数实现运算符重载),但两种方式不能同时出现。

(Ⅰ)、成员函数实现加号运算符重载

        我们在成员函数中加入Person operator+(Person& p)成员函数,如下。operator+是固定写法,使用“operator运算符”,我们就可以实现对该运算符的重载。

class Person
{
public:
	Person(int a)
	{
		m_a = a;
	}
	Person operator+(Person& p)
	{
		Person ptemp(0);
		ptemp.m_a = this->m_a + p.m_a;
		return ptemp;
	}
	int m_a;
};

         需要注意的是函数运算符重载中,函数的返回类型为Person,这里是不能写成Person&引用类型的。因为我们这里是创建了一个临时对象ptemp,一旦该函数调用完,对象ptemp就被释放了。否则就会造成内存的非法访问。

(Ⅱ)、全局函数实现加号运算符重载

        我们也可以使用全局函数实现加号运算符重载。如下:

Person operator+(Person& p_1, Person& p_2)
{
	Person ptemp(0);
	ptemp.m_a = p_1.m_a + p_2.m_a;
	return ptemp;
}

        与成员函数实现加号运算符重载一样,这里依然返回值为Person类型,而不是Person&

(Ⅲ)、对加号运算符重载的总结

        其实,在调用p3 = p1 + p2时,C++会自动调用相应的全局函数或成员函数。对于成员函数,C++会翻译为p3=p1.operator+(p2),所以我们可以发现,对于成员函数实现运算符重载时,第一个操作数p1是调用成员函数所属的对象,而p2才是被传入的参数。这点的理解尤为重要。

        对于全局函数实现加号运算符重载,系统会理解为p3=operator+(p1,p2)。有人可能会有疑问,为什么系统会理解为p3=operator+(p1,p2),而不是p3=operator+(p2,p1)。很好,这里看似是一个小问题,“不都是两个数相加吗?谁前谁后不都一样吗。”但是,我们想个问题,这里实现的是两个数相加,但是如果是两个数相减呢?如果传入参数的顺序不一样,那么结果就会大不一样。我们这里不妨用代码试验一下。

        这里还是对于上面同样一个类,我们实现减法运算符的重载。这里,p1传参为20,p2传参为10。我们计算p3=p1-p2的结果。如果系统传参顺序为p3=operator-(p1,p2),那么计算结果就为10;如果系统传参顺序为p3=operator-(p2,p1),那么计算结果就为-10;

Person operator-(Person& p_1, Person& p_2)
{
	Person ptemp(0);
	ptemp.m_a = p_1.m_a - p_2.m_a;
	return ptemp;
}

void test01()
{
	Person p1(20);
	Person p2(10);
	Person p3(0);
	p3 = p1 - p2;
	cout << p3.m_a << endl;
}
int main()
{
	test01();
	return 0;
}

        可以得到,系统计算结果为10,而不是-10。说明系统会理解为p3=operator-(p1,p2)即对于全局函数实现操作符重载,左操作数为函数的第一个参数,右操作数为第二个参数。

        另外还需注意的是:对于同一个运算符同一种类型的重载,不能全局函数与成员函数实现都存在,否则系统就会报错。

        除了用运算符重载实现同一种类两个对象的相加,我们也可以实现,不同类型之间的相加,如:Person类与整数的相加。

Person operator+(Person& p1, int a)//可以实现类与整型的相加
{
	Person temp;
	temp.m_a = p1.m_a + a;
	return temp;
}

或牛类与马类的相加,返回值为人类。 

class Person//定义人类
{
public:
	Person(int a)
	{
		m_a = a;
	}

	int m_a;

};
class Horse//定义马类
{
public:
	Horse(int a)
	{
		m_a = a;
	}
	int m_a;
};
class Oxen//定义牛类
{
public:
	Oxen(int a)
	{
		m_a = a;
	}
	Person operator+(Horse p)//运算符重载,在Oxen类中实现
	{
		Person ptemp(0);
		ptemp.m_a = this->m_a + p.m_a;
		return ptemp;
	}
	int m_a;
};
void test01()
{
	Oxen p1(20);
	Horse p2(10);
	Person p3(0);
	p3 = p1 + p2;
	cout << p3.m_a << endl;
}
int main()
{
	test01();
	return 0;
}

2.左移运算符重载

        我们可以发现,在实现加号运算符重载的过程中,我们要打印输出对象中属性的值通常需要用到下面这个代码:

	cout << p3.m_a << endl;

        但是我们有没有更好的方法去实现对对象中属性的打印输出呢? 答案是有的。比如我们可以通过对左移运算符<<重载来实现对对象属性的打印输出,代码就可以简化为:

cout<<p3<<endl;

        那么现在的问题就是该如何来实现对左移运算符的重载。我们知道实现运算符重载有两种方式,全局函数或成员函数。如果用成员函数来实现运算符重载,C++会隐式地将左侧的对象作为调用者,因此在成员函数中可以直接访问该对象的成员变量和其他成员函数。例如,对于 a + b,如果 a 是一个类的对象,C++会自动调用 a.operator+(b) 。那我们在思考下,对于左移运算符,cout是它的左操作数,那我们如果要实现对左移运算符的重载的话,我们是不是就要在cout这个类中定义运算符重载的成员函数。显然这是不可能的。所以对于左移运算符的重载,我们是不能通过成员函数来实现的。

        既然不能通过成员函数来实现,那我们就只能通过全局函数来实现左移运算符重载了。对于一个全局函数除了函数体外,我们还需要知道什么?

        ①首先是函数名,这个我们十分清楚,对于运算符重载,函数名不久是operator加上相应的操作数吗?那么对于左移运算符,函数名就是operator<<。②其次,我们还需要知道函数的传参类型。我们知道,对于全局函数实现运算符重载,运算符的左操作数就是全局函数要传入的第一个参数,右操作数就是要传入的第二个参数。那么我们就知道了,对于左移运算符的重载,第一个参数一定是cout,第二个参数是类对象。对于cout类型为ostream,对于对象p1,类型为Person。那是否可以写成(ostream cout,Person p1)呢?不能,因为当我们写成ostream cout时,本质上是通过拷贝构造函数生成了一个对cout对象的临时拷贝。而ostream类中的拷贝构造函数是Protected权限类型的,系统无法拷贝。所以编译器就会提示错误。这里可以采取引用即ostream&。③最后是返回类型,对于左移运算符我们希望在cout<<p3后还可以打印一个换行endl,即链式编程。所以我们就需要,打印完p1的内容后,能够返回一个cout。这样才能实现在p1打印输出后,仍然可以打印其他内容。如下图。仍然要注意的是,返回类型依然为引用,原因与上面相同。当函数返回类型为ostream时,本质还是进行了一次对cout对象的拷贝。

        所以现在我们知道了函数的返回类型,函数名以及传参类型,剩下的就是函数体。我们要实现对一个对象属性的打印, 只需在函数体内将传参对象的属性进行打印就好。完整版代码如下:


class Person
{
public:
	Person(int a, int b)
	{
		m_a = a;
		m_b = b;
	}
	//不能在类内创建成员函数来实现操作符重载,因为无论怎么创建,在调用时一定是p1<<
	//void operator<<()
	//{

	//}

//private:
	int m_a;
	int m_b;
};
//全局函数实现操作符重载
ostream& operator<<(ostream& cout, Person& p)//ostream里面的拷贝构造函数是protected的,无法拷贝。所以要采取引用,这样在调用的时候并不需要创建一个临时变量
{
	cout << p.m_a<<" "<<p.m_b;
	return cout;
}
void test()
{
	Person p1(10,20);
	cout << p1 << endl;
}
int main()
{
	test();
	return 0;
}

3.赋值运算符重载 

        下面我们要讲的是赋值运算符重载。在默认情况下,C++编译器会给一个类添加4个函数。分别为:

(1)、无参构造函数

(2)、析构函数

(3)、拷贝构造函数(对属相进行值拷贝)

(4)、赋值运算符operator=,对属性进项值拷贝

        首先,我创建了一个Person类 。同时定义了一个函数test01(),在test01()函数中创建三个对象p1p2p3,想要把p1的值赋给p2p3。在默认情况下,即使你不定义赋值运算符,C++编译器也会自己生成一个赋值运算符重载函数,对属性进行至拷贝。所以下面的代码并不会报错。

class Person
{
public:
	Person(int age)
	{
		m_age=age;
	}
	
	int m_age;
};

void test01()
{
	Person p1(10);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1;
	cout << p1.m_age << endl;
	cout << p2.m_age << endl;
    cout << p3.m_age << endl;
}

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

        下面我将介绍三点关于赋值运算符重载的注意事项。

        ①、首先是我们要学会区分赋值运算符重载与构造函数的区别。如下代码的输出结果会是什么?Person p2=10Person p3=p1p4=p1这三个语句都有赋值号“=”,那么他们都分别会调用什么函数?

class Person
{
public:
	Person(int a)
	{
		cout << "有参构造函数的调用" << endl;
		m_a = a;
	}
	Person(Person& p)
	{
		cout << "拷贝构造函数的调用" << endl;
		m_a = p.m_a;
	}
	void operator=(Person& p)
	{
		cout << "赋值运算符重载函数的调用" << endl;
		m_a = p.m_a;
	}
	int m_a;
};
int main()
{
	Person p1(10);
	Person p2 = 10;
	Person p3 = p1;
	Person p4(20);
	p4 = p1;
	return 0;
}

         我们看下打印结果,可以看到Person p2=10Person p3=p1都调用的是构造函数,而不是赋值运算符重载函数的调用。而p4=p1调用的是赋值运算符重载函数。这里要求我们区分初始化与赋值之间的关系。Person p2=10Person p3=p1系统会理解为Person p2=Person(10)Person p3=Person(p1)。这里本质上是调用相应的构造函数对对象进行初始化。

        ②、其次是赋值运算符重载必须是成员函数,这是因为即使我们自己不在类内定义赋值运算符重载的成员函数,系统也会自己给我们生成一个相应的重载成员函数。如果我们再定义全局函数,那么在使用p1=p2时,就会产生二异性。         ③、要注意系统默认的赋值运算符重载时进行的是值拷贝。当我们在堆区开辟内存时,非常有可能出现错误。比如下面这个代码在运行过程中就会出错。

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(10);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1;
	cout << *p1.m_age << endl;
	cout << *p2.m_age << endl;
}
int main()
{
	test01();
	return 0;
}

         报错的原因是因为系统默认的赋值构造函数是进行的值拷贝,即p2拷贝的是p1指向堆区内存空间。当函数test01调用完后,p2中m_age指向的堆区内存就被释放了,而p1中的m_age仍指向堆区那片区域,但是这片区域已经被销毁了,于是就会出现内存的非法访问。

        所以,我们类中,如果有在堆区开辟的内存空间,我们一定要自己构建拷贝构造函数与赋值运算符重载函数,同时,在进行赋值时,要先释放之前堆区的空间,再赋值,以免产生内存碎片。正确的代码如下:

class Person
{
public:
	Person(int age)
	{
		m_age=new int(age);
	}
	Person(Person& p1)
	{
		this->m_age = new int(*p1.m_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(10);
	Person p2(20);
	Person p3(30);
	p3 = p2 = p1;
	cout << *p1.m_age << endl;
	cout << *p2.m_age << endl;
}
int main()
{
	test01();
	return 0;
}

 4.递减/递增运算符的重载

        这里以递增运算符为例,递增运算符又可分为前置递增与后置递增。我们分别来介绍这两种递增运算符的重载。

(Ⅰ)前置递增运算符的重载

        代码如下,为了实现链式编程,我们需要返回值为引用类型。

class Person
{
public:
	Person(int a)
	{
		m_a = a;
	}
	Person& operator++()
	{
		m_a++;
		return *this;
	}
	int m_a;
};
int main()
{
	Person p1(10);
	Person p2(p1);
	++(++p1);
	cout << p1.m_a << endl;
	cout << p2.m_a << endl;
	return 0;
}

(Ⅱ)后置递增运算符的重载 

        代码如下:

class Person
{
public:
	Person(int a)
	{
		m_a = a;
	}
	Person& operator++()//前置加加
	{
		m_a++;
		return *this;
	}
	Person operator++(int)//后置加加
	{
		Person temp(this->m_a);
		m_a++;
		return temp;
	}
	int m_a;
};

int main()
{
	Person p1(10);
	Person p2(20);
	++(++p1);
	cout << p1.m_a << endl;
	cout<<(p2++).m_a<<endl;
	cout << p2.m_a << endl;
	return 0;
}

 (Ⅲ)区别及注意事项

        可以看出,前置加加与后置加加有很大的区别。主要表现在以下几点:

        ①、前置加加返回值为Person&引用类型,而后置加加返回值为Person为什么会有这个区别呢?因为前置加加有个特性是先加加后使用,而后置加加是先使用,后加加。对于前置加加,我们可以加加后返回原对象(*this),但是对于后置加加,是先返回当前对象的值,然后再将对象的值增加1。为了实现这一点,必须在返回之前保存当前对象的状态。所以我们创建了一个临时对象temp,但是对于temp,我们返回时不能返回引用,因为函数调用完这部分内存就被释放了。返回引用或指针都可能造成内存的非法访问。

        ②、前置加加没有占位参数,而后置加加有占位参数。这是不是意味着我们在要用后置加加时要传参呢?答案是否。后置加加必须有参数,C++编译器要求,需要在后置运算符重载函数中加参数“int”(这里只能用int类型来做占位参数),这个类型在此除了以示区别之外并不代表任何实际含义;如果不加,编译器无法区分是前置++,还是后置++,导致报错。

        ③、如果使用后置递增,因为返回值不是引用类型,所以就不能进行链式编程了或链式编程会出现错误。按理来说,对p2进行了两次后置加加操作,应该输出也为12,但是因为返回值不是引用类型。所以产生了错误。

        其实,对于C++来说,无论任何变量类型的后置加加操作,系统都不允许进行链式编程具体为什么我也没搞懂)。比如下面这个代码,有知道的小伙伴可以评论区留言。

         ④、前置加加与后置加加都可以用全局函数来实现。如下:

Person& operator++(Person& p)//全局函数实现加加也是可以的
{
	p.m_a++;
	return p;
}
Person operator++(Person& p,int)//全局函数实现后置加加也是可以的
{
	Person temp(p.m_a);
	p.m_a++;
	return temp;
}

5.关系运算符重载 

         这个相对简单,只不过注意返回类型为bool类型就可以。

class Person
{
public:
	Person(int age)
	{
		m_age = age;
	}

	bool operator==(Person p)
	{
		if (this->m_age == p.m_age)
		{
			return true;
		}
		return false;
	}

	int m_age;
	
};
//bool operator==(Person p1, Person p2)//也可通过全局函数实现重载
//{
//	if (p1.m_age == p2.m_age)
//	{
//		return true;
//	}
//	return false;
//}
void test()
{
	Person p1(10);
	Person p2(10);
	if (p1 == p2)
	{
		cout << "p1==p2" << endl;
	}
	else
	{
		cout << "p1!=p2" << endl;
	}
}
int main()
{
	test();
	return 0;
}

6.函数调用运算符的重载 

        函数调用运算符“()”,的使用与函数的调用相似,所以又称为仿函数。这里要注意的是,我们必须通过创建一个对象(匿名对象也可)来调用函数调用运算符。

class m_Add
{
public:
	void operator()(int b, int c)
	{
		cout << b + c << endl;
	}

};
void test01()
{
	m_Add Add;
	Add(10, 20);。
	m_Add()(10, 20);//匿名对象也可以,只不过匿名对象创建好后,在该行执行完就结束了
	Add(10, 20);
}
int main()
{
	test01();
	return 0;
}

        同时,函数调用运算符的重载必须是成员函数。全局函数实现时会报错。 


总结

        本文对C++有关运算符重载的知识进行了详细归纳。

        可总结为几点:

①、除了左右移运算符、赋值运算符、函数调用运算符以外,其余函数都可以用全局函数或成员函数实现对运算符的重载。

②、左右移运算符重载只能使用全局函数。

③、赋值运算符与函数调用运算符重载只能使用成员函数。

等等..............(这里就不再详细列举了,大家可以回顾下文中的红色字体)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值