运算符重载(加减运算符、前置加加(减减)后置加加(减减)运算符、赋值运算符、输入输出运算符、关系运算符、函数调用)

编译器对于一个类会默认生成以几种函数
1.默认构造函数(空形参,空函数体)
2.默认拷贝构造函数(浅拷贝,也叫值拷贝、字节拷贝)
3.析构函数(空形参,空函数体。析构函数要求形参列表必须是空的,所以析构函数不能重载)
4.赋值函数(两个对象赋值,也是以浅(值、字节)拷贝的形式)
讲述以上这四种默认生成的函数的优秀博客点击此处

编译器会对内置类型(比如char,int,short…)进行加减乘除运算符计算,但是我们自定义类型,有些运算编译器不知道怎么做。我们自定义类型需要进行这些运算的时候,需要我们重载运算符。
运算符重载在不同情况下可以选择用成员函数重载或者全局函数重载。本篇博客讲述记录以下几种运算符重载
1.加减运算符重载。
2.前置加加(减减)后置加加(减减)运算符重载。
3.赋值运算符重载。
4.左移右移运算符重载(输入输出运算符重载)。
5.关系运算符重载(大于、小于、等于、不等于)。
6.函数调用运算符重载(重载(),使用的时候也叫仿函数)。
注意
(1)运算符重载依然是函数,而且本身就是重载函数,所以函数符重载函数仍然可以进行函数重载,且依然遵循函数重载的规则。
(2)".*"、"?:"、“sizeof”、"::"、"."这几个运算符无法进行重载。
(3)不能对内置类型的运算符进行重载。即不能对int、double等类型的运算符进行重载。换句话说,运算符重载,参数必须至少有一个自定义类型。
(4)不能创建新的运算符。只能对已有的运算符进行重载,不能结合符号创建新的运算符。

一、加减运算符重载

运算符重载的统一函数名:oprerator“运算符”。
比如
重载+:返回类型 operator+(形参列表)
重载-:返回类型 operator-(形参列表)
…其他运算符重载函数的函数名类推。
运算符重载函数的返回类型很重要,要根据实际考虑而定。

1.加法运算符重载

(1)以全局函数的形式进行加法运算符重载

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
public:
	int _a;
	int _b;
};

//全局函数,”+“重载
A operator+(A a, A b)
{
	A tmp;
	tmp._a = a._a + b._a;
	tmp._b = a._b + b._b;
	return tmp;
}
int main()
{
	A a1(1, 2);
	A a2(9, 10);

	A a4 = operator+(a1, a2);//语句①
	A a5 = a1 + a1 + a2 + a2;//语句②

	cout << a4._a << "   " << a4._b << endl;
	cout << a5._a << "   " << a5._b << endl;

	return 0;
}

在这里选择用全局函数重载"+“运算符的。下面再显示用成员函数重载”+"运算符。
运行结果:
在这里插入图片描述

调用运算符重载函数的语句①是显示的调用运算符重载函数,也是我们正常调用函数的写法。语句②是简便写法方式。而且发现可以连续做加法。

(2)以成员函数的方式进行加法运算符重载
下面用成员函数重载"+"运算符。注意,成员函数有this指针,所以成员函数已经算是有一个参数了,加数是双目运算符,在这里已经隐藏的this指针参数了,形参显示的写一个就好了。
代码:

class A
{

public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	A operator+(A a)//成员函数重载"+"运算符
	{
		A tmp;
		//注意,可以不用this指针指向,可以直接tmp.a = _a+a.a
		tmp._a = this->_a + a._a;
		tmp._b = this->_b + a._b;
		return tmp;
	}
public:
	int _a;
	int _b;
};


int main()
{
	A a1(1, 2);
	A a2(9, 10);

	A a4 = a1.operator+(a2);//语句①
	A a5 = a1 + a1 + a2 + a2;//语句②
	A a6 = a1.operator+(a1).operator+(a2).operator+(a2);//语句③

	cout << a4._a << "   " << a4._b << endl;
	cout << a5._a << "   " << a5._b << endl;
	cout << a6._a << "   " << a6._b << endl;

	return 0;
}

语句①是我们平常调用普通成员函数的写法。
语句②是简便写法(一般都用简便写法调用运算符重载函数),真实调用方式和语句 ③一样。
对象a5和对象a6是调用编译器给的默认拷贝构造函数构造的。
运行结果:
在这里插入图片描述

2.减法运算符重载

不管是以成员函数方式重载还是以全局函数方式重载,减法运算符和加法运算符重载一摸一样。把+改成-就好了。为了减少篇幅和重复代码,这里就不粘贴修改后的代码了。

二、前置加加(减减)后置加加(减减)运算符重载

1.前置加加(减减)运算符重载

(1)全局函数形式进行前置加加(减减)运算符重载

根据前面所提到的。前置++的运算符重载函数的形式应该是这种:返回类型 operator++(形参列表)。
考虑一下形参列表,如果用全局函数形式,那么肯定得把需要对其进行前置++的对象传给函数,所以形参列表有一个参数,还有就是我们是对对象本身进行修改值操作,所以我们得以引用的形式传进去。
考虑一下返回类型,考虑一下觉得以临时对象的形式返回可以,以引用形式返回也行。隐隐觉得引用形式为返回类型更好,但是又不觉得哪里好。实际上一定要以引用形式返回。下面进行解释。考虑好形参列表和返回类型,代码就很好实现,先举例以临时对象的形式为返回类型:
代码:

class A
{

public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	
public:
	int _a;
	int _b;
};

A operator++(A& a)
{
	++a._a;
	++a._b;
	return a;
}

int main()
{
	A a(1, 2);
	++a;
	cout << a._a << "   " << a._b << endl;//OK
	++(++a);//error

	return 0;
}

发现++(++a)错误。分析一下:
++(++a)相当于语句operator++(operator++(a))
operator++(a)返回的是一个临时对象,临时对象是一个存放在将要销毁的空间中的对象。不允许对其进行操作,而且必须是以值拷贝的形式对临时对象进行接收。所以把临时对象当成右值也没什么错。对临时对象再进行前置++是不允许的。所以我们返回类型不要以临时对象的形式返回,我们要以引用形式返回,既然是模仿编译器对内置类型的前置++,我们就得把操作做的像内置类型一样。做高仿,我们是认真的。
修改后的代码如下:

class A
{

public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	
public:
	int _a;
	int _b;
};

A& operator++(A& a)
{
	++a._a;
	++a._b;
	return a;
}

int main()
{
	A a(1, 1);

	++a;
	cout << a._a << "   " << a._b << endl;
    ++(++a);//相当于operator++(operator++(a),里面的operaator++(a)会返回a本身,所以相当于又进行一次operator++(a)
	cout << a._a << "   " << a._b << endl;

	return 0;
}

运行结果:
在这里插入图片描述
别说++(++a)了。一次性多进行几次前置++都没问题。
前置–和前置++一模一样,把++改成–就好了。

(2)成员函数形式进行前置加加(减减)运算符重载
和加法(减法)运算符重载一样考虑,作为成员函数,隐含this指针,而前置++只需要一个参数,所以this指针就够了,成员函数形式我们不需要再显示添加形参了。至于返回类型,和全局函数进行前置++运算符重载考虑一样,以引用形式返回。
注意:如果我们对前置++进行了全局函数形式重载,同时又进行了成员函数形式重载,那么会让编译器调用前置++的时候不知道调用哪一个。所以对于同一种类型,不要让它的前置++运算符重载既保留全局函数形式重载又保留成员函数形式重载。其他运算符也一样要注意。
以成员函数形式进行前置加加运算符重载代码:

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	A& operator++()//成员函数形式进行前置++运算符重载
	{
		++_a;
		++_b;
		return *this;
	}
public:
	int _a;
	int _b;
};

int main()
{
	A a(1, 1);

	++a;
	cout << a._a << "   " << a._b << endl;
    ++(++a);//相当于operator++(operator++(a),里面的operaator++(a)会返回a本身,所以相当于又进行一次operator++(a)
	cout << a._a << "   " << a._b << endl;

	return 0;
}

运算结果:
在这里插入图片描述
后置–和后置++一模一样,把++改成–就好了。

2.后置加加(减减)运算符重载

由于后置加加和前置加加没办法区分,因为形参列表一样,都是只有一个参数。编译器为了区分前置加加和后置加加,规定后置加加的形参列表要多一个占位形参int。这样形参列表就不同了。所以后置加加的函数形式和前置加加的函数形式不同就在于形参多了int占位。
再来分析一下返回类型。我们需要对对象进行后置++,那么我们返回的肯定是++前的值。所以我们不能以引用形式返回。必须创建一个临时对象保存要被后置++的对象的原始值。对原对象进行++操作,返回临时对象。

(1)全局函数形式进行后置加加运算符重载

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}

public:
	int _a;
	int _b;
};

A operator++(A& a, int)
{
	A tmp = a;
	++a._a;
	++a._b;
	return tmp;
}

int main()
{
	A a(1, 1);
	A b = a++;
 
	cout << a._a << "   " << a._b << endl;
	cout << b._a << "   " << b._b << endl;
	
	return 0;
}

运行结果:
在这里插入图片描述
以临时对象返回,所以不能像前置++那样进行连续后置++内置就算是内置类型也不能连续后置++。做高仿,我们是认真的。
后置–和后置++一模一样,把++改成–就好了。

(2)成员函数形式进行前置加加(减减)运算符重载
像前面分析的一样,把函数放到类里面就好了。成员函数有this指针。我们只需要把占位形参int写到形参里面就好了。

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	A operator++(int)
	{
		A tmp = *this;
		++_a;
		++_b;
		return tmp;
	}
public:
	int _a;
	int _b;
};

int main()
{
	A a(1, 1);
	A b = a++;
 
	cout << a._a << "   " << a._b << endl;
	cout << b._a << "   " << b._b << endl;
	
	return 0;
}

运行结果:
在这里插入图片描述
成员函数进行后置–重载和后置++一模一样,把++改成–就好了。

以上前置加加(减减)后置加加(减减)记录完毕。
但是请看以下代码,请先猜测一下结果:

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	A operator++(int)//后置++运算符重载
	{
		A tmp = *this;
		++_a;
		++_b;
		return tmp;
	}
	A& operator++()//前置++运算符重载
	{
		++_a;
		++_b;
		return *this;
	}
public:
	int _a;
	int _b;
};

int main()
{
	A a(1, 1);
 
	//在输出语句进行后置++
	cout << (a++)._a << "   " << a._b << endl;
	cout << a._a << "   " << a._b << endl;

	//在输出语句进行前置++
	cout << (++a)._a << "   " << a._b << endl;
	cout << a._a << "   " << a._b << endl;

	return 0;
}





运行结果如下:
在这里插入图片描述

三、赋值运算符重载

赋值运算符,涉及浅拷贝深拷贝问题。只要是赋值,就涉及深拷贝浅拷贝问题。浅拷贝是值拷贝,也叫字节拷贝。但是如果有在堆上申请空间的话,那么浅拷贝的后果就是多个指针指向这个堆空间,两个对象共用一个堆空间。析构的话,就会存在重复释放堆空间,发生崩溃。所以涉及堆空间的申请,一定要注意深浅拷贝问题。

1.普通赋值运算符重载

编译器给的赋值运算符是浅拷贝。只对变量里面的值进行字节拷贝,即不论变量里面存放是普通值还是地址值,都会一个一个字节的拷贝赋值。而赋值运算符注意,必须得是成员函数。也就是不能以全局函数的形式对赋值运算符进行函数重载。(个人猜测,编译器这样限制是由于只允许我们赋值运算符只能存在同类的对象与同类的对象之间的赋值。不能让两个自定义类型之间存在赋值。)
由于有时候需要进行连续赋值操作,所以我们返回类型以引用方式返回。

class A
{
public:
	A() {}
	A(int a, int b) :_a(a), _b(b) {}
	A& operator=(const A& b)
	{
		_a = b._a;
		_b = b._b;
		return *this;
	}
public:
	int _a;
	int _b;
};

int main()
{
	A a(1, 1);
	A b;
	A c;
	A d = a;//调用的是系统默认的拷贝构造函数
	b = c = a;//调用=运算符重载函数
	cout << b._a << "   " << b._b << endl;
	cout << c._a << "   " << c._b << endl;

	return 0;
}

运行结果:
在这里插入图片描述
注意:对于=运算符,编译器从右边执行,也就是先把a赋值给c再把c赋值给b。如果从左边开始赋值的话,那么b得到的会是c的未初始化的值,c得到的是a的值。这样连续赋值起不到作用。

2.需要深拷贝的赋值运算符重载

1.普通赋值运算符(浅拷贝)
为什么需要考虑深拷贝。我们先来看以下代码:

class A
{
public:
	A() {}
	A(int a, int b)
	{
		_ptr = new int(a);
		_val = b;
	}

	A& operator=(const A& b)
	{
		_ptr = b._ptr;
		_val = b._val;

		return *this;
	}

	~A() 
	{
		if (this->_ptr != nullptr)
		{
			delete _ptr;
		}
	}
public:
	int* _ptr;
	int _val;
};

int main()
{
	A a(1, 1);
	A b;
	A c = a;//调用默认拷贝构造函数
	b = a;//调用赋值运算符重载
	return 0;
}

赋值运算符重载函数有一句_ptr = b._ptr;
这句代码将赋值语句的左对象的指针指向了右对象的堆空间。
析构函数有一句delete _ptr;
这句代码对对象_ptr指针指向的堆空间进行释放
主函数有两句代码:A c = a; b = a;
这两句的代码的效果一样,都会使 c(b)的_ptr指针指向a的_ptr 指针指向的堆空间。

结果就是一个堆空间被多个对象的_ptr指针指向,对象的生存期到的时候,调用析构函数,堆空间第一次释放没问题,第二次释放就会崩溃。解决这个问题的办法就是我们进行深拷贝。深拷贝构造函数在拷贝构造函数里讲过了。这里记录赋值运算符重载解决问题。我们暂且叫深赋值浅赋值。(其实拷贝问题和赋值问题是同一种问题。)

2.赋值(深拷贝)运算符重载
代码:

class A
{
public:
	A() {}
	A(int a, int b)
	{
		_ptr = new int(a);
		_val = b;
	}

	A& operator=(const A& b)
	{
		_ptr = new int(*b._ptr);//给_ptr一个新的堆空间,用b._ptr指向的堆空间的值初始化_ptr的堆空间的值
		_val = b._val;

		return *this;
	}

	~A() 
	{
		if (this->_ptr != nullptr)
		{
			delete _ptr;
		}
	}
public:
	int* _ptr;
	int _val;
};

int main()
{
	A a(1, 1);
	A b;

	b = a;//调用赋值运算符重载
	cout << a._ptr << "   " << *a._ptr << endl;
	cout << b._ptr << "   " << *b._ptr << endl;
	return 0;
}

注意=运算符重载函数的代码,给需要赋值的对象的_ptr指针分配了新空间。
所以两个对象的_ptr指针指向的堆空间不同,调用析构函数时,不会出现对同一个堆空间释放两次的情况。运行结果如下:
在这里插入图片描述

四、输入输出运算符重载

重载之前,首先要知道输入输出的类型,输出cout的类型为ostream(即output stream输出流),输入cin的类型为istream(input stream输入流)。而且全局对象,只有一个。
分析以下输出运算符重载。输出运算符,就是全局对象调用operator<<构成的。cout就是全局对象。cout<<a本质就是operator<<(cout,a);所输出运算符重载形式为:
返回类型 operator<<(ostream& out, const T& a);
其中,ostream是cout的类型,out相当于是cout的别名,T是需要输出的对象的类型名,a是需要输出的对象。
那么返回类型应该是什么?我们输出的时候可以连续输出。所以我们可以连续 调用这个输出运算符,调用这个输出运算符的是输出流(ostream)cout,所以我们应该引用形式返回ostream类型。即:
ostream& operator<<(const ostream& out,const T& a);
那么就剩函数体了,下面给出代码。

1.左移运算符重载

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
public:
	int _a;
	string _str;
};

ostream& operator<<(ostream& out, const A& a)
{
	out << a._a << "   " << a._str;
	return out;
}

int main()
{
	A a(1, "aaa");
	A b(2, "bbb");
	cout << a << "   " << b << endl;
	return 0;
}

运算结果:
在这里插入图片描述
上面是以全局函数的形式对输出运算符进行重载。那么以成员函数的形式对输出运算符进行重载呢?成员函数有this指针,所以对象不用显示再写了,所以输出流ostream& out要显示的写到形参中。所以函数格式为:
ostream& operator<<(const ostream& out);
如下代码:

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	ostream& operator<<(ostream& out)
	{
		out << this->_a << "   " << this->_str;
		return out;
	}
public:
	int _a;
	string _str;
};

写出来之后,应该发现调用问题。对象调用,对象在前面,那么cout变成在后面了。调用就是以下形式:

int main()
{
	A a(1, "aaa");
	A b(2, "bbb");
	a << cout;
	return 0;
}

可以输出出来,但是肯定不合适。我们是做运算符重载,模仿编译器的运算符操作方式,对自定义类型重载合适的运算符。模仿我们就要求高仿,使用方式得和编译器对内置类型的运算符操作一样。所以输出运算符重载写成成员函数的方式不合适。得写成全局函数的方式。
一般类的属性是私有,我们要用输出重载函数输出对象的私有属性。所以一般我们都得把输出运算符重载函数设置成类的友元函数。

2.右移运算符重载

在这里,我们把类属性写成私有,输入输出运算符重载都按规范要求写成全局函数形式,且声明为类的友元函数:
代码:

class A
{
	friend istream& operator>>(istream& in, A& a);
	friend ostream& operator<<(ostream& out, const A& a);
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
private:
	int _a;
	string _str;
};

istream& operator>>(istream& in,A&  a)//输入运算符重载
{
	in >> a._a >> a._str;
	return in;
}

ostream& operator<<(ostream& out, const A& a)//输出运算符重载
{
	out << a._a << "   " << a._str;
	return out;
}

int main()
{
	A a;
	A b;
	cout << "please input two data,data form(int,string):" << endl;
	cin >> a >> b;//调用的是输入运算符重载函数
	cout << a << "   " << b << endl;//调用的是输出运算符重载函数

	return 0;
}

运行结果:
在这里插入图片描述

五、关系运算符重载

对于关系运算符重载,返回类型肯定是bool类型的,这也是为什么不能连续比较大小关系的原因。双目运算符,参数需要两个。如果是以成员函数的方式进行关系运算符重载,由于隐含this指针参数,那么显示的写一个形参就好了。对于上面所讲的那些运算符来说,关系运算符重载比较简单。

1.大于号小于号运算符重载

(1)以全局函数的方式写大于(小于)运算符重载

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
public:
	int _a;
	string _str;
};

bool operator>(const A& a, const A& b)
{
	return a._a > b._a;
}

(2)以成员函数的方式写大于(小于)运算符重载

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	bool operator>(const A& b)
	{
		return this->_a > b._a;
	}
public:
	int _a;
	string _str;
};

2.等于和不等于运算符重载

(1)以全局函数的方式写等于(不等于)运算符重载
等于号的重载:

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	
public:
	int _a;
	string _str;
};

bool operator==(const A& a, const A& b)
{
	return a._a == b._a;
}

不等于号的重载:

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	
public:
	int _a;
	string _str;
};

bool operator!=(const A& a, const A& b)
{
	return a._a != b._a;
}

(2)以成员函数的方式写等号(不等号)运算符重载
等号的运算符重载:

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	bool operator==(const A& b)
	{
		return this->_a == b._a;
	}
public:
	int _a;
	string _str;
};

不等号运算符重载:

class A
{
public:
	A() {}
	A(int a, string str):_a(a),_str(str){}
	bool operator!=(const A& b)
	{
		return this->_a != b._a;
	}
public:
	int _a;
	string _str;
};

比较的标准是我们来定的,如果想要以其他标准(属性)比较,我们就把函数体内换一下比较标准就好了。

六、函数调用运算符重载("()"重载,使用的时候被称为法仿函数)

函数调用运算符重载是我在其他网站上看别人这样称呼的,我之前都成为小括号()重载,专业术语就应该叫作函数调用运算符重载。因为类调用这个重载函数的时候,和函数调用极其相似,所以也被称为仿函数。当重载()时,不是创造了一种新的调用函数的方式,而是创造了一个可以传递任意数目参数的运算符函数。
注意:operator()必须是成员函数!
下面举例()重载。
如下代码:

class A
{
	
public:
	A() {}
	A(int a, string str) :_a(a), _str(str) {}
	int operator()()
	{
		return this->_a;
	}
private:
	int _a;
	string _str;
};

int main()
{
	A a(1, "aaa");
	A b(2, "bbb");
	if (a() > b())
	{
		cout << "a > b" << endl;
	}
	else if (a() == b())
	{
		cout << "a == b" << endl;
	}
	else
	{
		cout << "a < b" << endl;
	}

	return 0;
}

以上代码。我们对()进行了运算符重载。我们让它返回比较大小的标准。即int类型的_a,这样我们就不需要写大于,小于,等于,不等于这些运算符重载函数了。写了一个就可以直接比较大小等于不等于。调用()运算符重载的时候,以"对象名()"的形式,而调用函数的时候,是"函数名()"的形式。所以很像是函数调用。所以使用时也叫仿函数。
运行结果:
在这里插入图片描述
()运算符重载,功能很强大,我们可以用这个重载做很多想做的事情。比如说想要再做个给对象_a属性加一个值的操作。
代码:

class A
{
	friend ostream& operator<<(ostream& out, const A& a);
public:
	A() {}
	A(int a, string str) :_a(a), _str(str) {}
	int operator()()
	{
		return this->_a;
	}
	void operator()(int val)
	{
		this->_a += val;
	}
private:
	int _a;
	string _str;
};

ostream& operator<<(ostream& out, const A& a)//输出运算符重载
{
	out << a._a << "   " << a._str;
	return out;
}

int main()
{
	A a(1, "aaa");
	A b(2, "bbb");
	
	a(10);
	cout << a << endl;

	return 0;
}

运行结果:
在这里插入图片描述
总之,可以重载()做很多事情。函数参数不同为标准可以重载operator()。

此外,我们还可以对"[]"、"*“重载,这里不进行记录了。但是都很容易写,这些运算符重载都一样。掌握两三个就可以达到举一反三的效果,对其他运算符重载也只是换碗不换药。
只不过记住赋值运算符”=“和”()"运算符只能作为成员函数重载。输入输出运算符重载需要规范的写成全局函数重载的方式。

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值