1.什么是运算符重载
在实际开发中,我们经常会自己定义数据类型,也经常需要对这些自定义数据类型进行+-*/等常用的计算。这时C++自带的这些操作符却并没有处理我们自定义数据类型的能力,所以就需要我们对这些操作符的功能进行扩展。而实现这一功能的方式就叫做操作符重载。
其实重载这个概念大家一定并不陌生,所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能。运算符重载同理,运算符重载使得用户自定义的数据以一种更简介的方式工作。
2.运算符重载的两种方法
运算符重载可以使用全局函数和成员函数或友元函数两种方式实现。那么为什会有两种实现方式呢?问题的本质到底是什么?
把成员函数 转成 全局函数 会多一个参数;全局函数 转换成 成员函数会少一个参数(面向对象思想与面向过程思想)
方法一:用成员函数重载运算符
- 运算符函数可以以类成员函数或友元函数的形式进行重载(注意:成员函数具有this指针,友元函数没有this指针)
- 运算符使用方式与重载方式无关。但传递参数的方式不同,所以应用场合也不同。
方法二:用全局函数或友元函数重载运算符
- 要记得全局函数无法访问类的私有成员!!
3.具体实现
3.1二元操作符重载(+ - * /):
运算符op使用情景:
左对象 op 右对象;
重载为成员函数:
左对象.operator op(右对象){}
左操作数由左对象通过this指针隐式的传递了,右操作数对象由参数显式传递。
重载为友元函数:
operator op(左操作数,右操作数){}
左右操作数都由参数传递。
例子:
使用友元全局函数实现“加号”重载,使用成员函数实现“减号”重载
class Complex
{
private:
int a;
int b;
public:
friend Complex operator+(Complex & c1, Complex &c2);
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}
//成员函数重载-操作符
Complex operator-(const Complex& right) {
Complex result;
result.a = this->a - right.a;
result.b = this->b - right.b;
return result;
}
};
//全局函数重载+操作符
Complex operator+(Complex & c1,Complex &c2) {
Complex result;
result.a = c1.a + c2.a;
result.b = c1.b + c2.b;
return result;
}
void main() {
int a = 0, b = 0;
int c;
//基础数据类型
c = a + b;
//a+bi 符数运算规则
Complex c1(1, 2), c2(3, 4);
Complex c3;
c3 = c1 + c2;
c3 = c1 - c2;
}
3.2一元操作符重载(++ --):
运算符op使用情景:
对象 op ; 或 op 对象;
重载为成员函数:
对象.operator op(){}
操作数由对象通过this指针隐式的传递。
重载为友元函数:
operator op(对象){}
操作数都由参数传递。
前置操作符重载(++i --i)
class Complex
{
private:
int a;
int b;
public:
friend Complex & operator++(Complex& c);
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}
//成员函数重载前置--操作符 --a
Complex& operator--() {
this->a--;
this->b--;
return *this;
}
};
//全局函数重载前置++操作符 ++a
Complex & operator++(Complex& c) {
c.a++;
c.b++;
return c;
}
后置操作符重载(i++ i--)
上面的前置操作符重载很好理解了,那我们现在写下一后置操作符重载。这时我们会发现一个严重的问题:后置操作符重载的函数怎么定义呀?函数名是相同的,参数也相同,这不符合重载的条件呀!!为了解决这个问题,C++提供了占位符。
Complex operator++(Complex & c, int){}
这样就满足重载的条件了,从而可以很好的区分前置操作符重载和后置操作符重载。
class Complex
{
private:
int a;
int b;
public:
friend Complex operator++(Complex& c, int);
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}
//成员函数重载后置--操作符 a--
Complex operator--(int) {
Complex temp=*this;
this->a--;
this->b--;
return temp;
}
};
//全局函数重载后置++操作符 a++
Complex operator++(Complex& c,int) {
Complex temp = c;
c.a++;
c.b++;
return temp;
}
3.3左移右移操作符重载(<< >>):
语法:
ostream& operator<<(ostream& out,自定义类型& obj);
istream& operator>>(ostream& out,自定义类型& obj);
全局函数方式重载:
class Complex
{
private:
int a;
int b;
public:
friend ostream & operator<<(ostream & out, Complex & obj);
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}
};
ostream & operator<<(ostream & out, Complex & obj) {
out << obj.a << " " << obj.b << endl;
return out;
}
成员函数方式重载:
通过前面的一些案例我们知道,成员函数方式重载需要写在对应类的内部。对于目前这个案例,相当于我们需要重载 cout.operator<<()这个函数,那么就需要写在ostream这个类中。那么问题来了,ostream和istream这个类的源码我们上那找去??拿不到啊,没法在类里重载啊!!
结论:输入输出操作符的重载只能使用友元函数进行重载,无法用成员函数的方式进行重载。
3.4等号操作符重载(=):
想完全搞懂等号操作符的重载需要先搞懂深拷贝和浅拷贝问题。没有基础的朋友可以看我的这篇文章:
https://blog.csdn.net/weixin_39568744/article/details/104884211
C++编译器提供的等号操作符也属于浅拷贝,比如obj1=obj2,如果对象中有指针变量时,默认等号操作只会把指针的值直接赋值,而不会开辟新的内存空间。
所以我们要进行等号操作符的重载来解决这个问题。首先写出重载的函数原型:
使用成员函数重载的方式: void operator=(类型 &obj);
在具体函数实现的过程中,有一点至关重要,就是一定要记得把旧的指针指向的内存空间释放掉!!
例:
void Array::operator=(Array & other) {
//释放旧指针所指空间
if (this->m_space != NULL) {
delete[] m_space;
this->m_length = 0;
}
//开辟新空间
this->length = other.length;
this->m_space = new int[this->length];
//赋值
for (int i = 0; i < this->length; i++) {
this->m_space[i] = other.m_space[i];
}
return;
}
经过上面的例子就可以实现 obj2=obj1 的深拷贝正确赋值。但如果我想实现 obj3=obj2=obj1 这种连等操作怎么呢??我们知道等号操作符是从右往左执行的,按照上面的重载方式 ,右面obj2=obj1会先执行重载函数,然后返回void,于是就变成了obj3=void,所以就出错了。解决办法很简单,既然是链式编程,那么我们就返回引用就可以了。
修改后的代码如下:
Array& Array::operator=(Array & other) {
//释放旧指针所指空间
if (this->m_space != NULL) {
delete[] m_space;
this->m_length = 0;
}
//开辟新空间
this->length = other.length;
this->m_space = new int[this->length];
//赋值
for (int i = 0; i < this->length; i++) {
this->m_space[i] = other.m_space[i];
}
return *this;
}
3.5中括号操作符重载([ ]):
假设我们有一个自己定义的数组Array,我们需要进行[]操作符重载以实现 Array[i]数组元素快速访问 和 Array[i]=9 快速修改等操作。要怎么做呢?首先根据我们的业务需求写出函数原型:
int& Array::operator[](int i);
我们以成员函数的方式进行重载,所以对象直接通过this指针隐式的传递了,参数只需要有一个int,因为我们业务中的索引是通过int类型;返回值设计为int &是因为我们的业务需求中需要有Array[i]=0;这种赋值功能,这个语句的本质就是操作符重载这个函数做左值,当函数返回值做左值时需要返回引用类型,所以我们返回值设计为了int&.
class Array
{
public:
Array(int length);
Array(const Array& obj);
~Array();
public:
int length();
private:
int m_length;
int *m_space;
public:
//函数返回值当左值需要返回引用
int& operator[](int i);
};
//重载[]
int& Array::operator[](int i){
return m_space[i];
}
3.6逻辑判断操作符重载( == ,!= ):
和3.5一样得情景中,添加逻辑判断操作符得重载;如下:
bool Array::operator==( Array& const a) {
if (this->length() != a.length()) {
return false;
}
for (int i = 0; i < this->length(); i++) {
if ((*this)[i] != a[i]) {
return false;
}
}
return true;
}
bool Array::operator!=(Array& const a) {
return !(*this == a);
}
3.7小括号操作符重载(())
除了上面我们常见的运算符重载外,()也可以进行重载。小括号操作符重载可以让对象看起来像函数调用一样使用。
class F
{
public:
int operator()(int a, int b) {
return a * 3 + b * 4 + k;
}
private:
int k = 0;
};
int main()
{
F f;
cout << f(1, 2);
}
4.运算符重载的机制总结
总结1:运算符重载本质是函数调用
总结2:运算符重载可以对运算符做出新的解释,但不会改变原有的基本语义:
- 不改变运算符的优先级
- 不改变运算符的结合性
- 不改变运算符所需要的操作数
- 不能创建新的运算符
总结3:友元函数实现操作符重载总结
- 友元函数重载运算符常用于左右操作数类型不同的情况
- 第一个参数需要隐式转换的情况下,使用友元函数重载
- 友元函数不能重载:“=”,“()”,“[]”,“->”
总结4:重载等号操作符总结
- 要先把被复制对象旧的内存释放
- 根据大小开辟新内存,并赋值
- 返回一个引用类型(为了实现连等)
总结5:最好不要重载&&和||操作符
- &&和||是非常特殊的操作符,因为他们内置实现了短路原则;
- 操作符重载是靠函数重载来实现的,操作数作为函数参数传递;
- C++的函数参数都会被求值,所以无法实现短路原则
总结6:不要忘记()操作符也可以重载
- 用于函数调用
- 记得区分是类的构造函数还是对象的()重载