我们前面学习到了函数可以重载,那运算符重载又是什么概念呢?
像 + , - ,* 这些运算符我们可以直接用于内置类型,但是如果用在类类型的对象上面的时候就不可以了,因为这个行为是编译器没有定义的,就会报错~
例:
#include<iostream>
using namespace std;
class Example
{
private:
int _a;
public:
Example(int a)
{
_a = a;
}
};
int main()
{
//运算符我们可以直接用于内置类型
int x = 1;
int y = 2;
cout << "x + y = " << x + y << endl;
Example e1(4);
Example e2(5);
//err
//类类型对象使用运算符时,这个行为是编译器没有定义的
cout << "e1 + e2 = " << e1 + e2 << endl;
return 0;
}
这个时候我们就给出了解决方案,使用运算符重载~
》 当 运算符被用于类类型的对象 时,C++允许我们通过 运算符重载的形式为运算符指定新的含义 。 》 C++规定 类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。
》 运算符重载是 具有特殊名字的函数 , 名字由operator和后面要定义的运算符共同构成 。 eg: operator+ 》 和其他函数⼀样,它也 具有其返回类型和参数列表以及函数体 。
》 重载运算符函数的 参数个数 和 该运算符作用的运算对象 数量相同~ 所以一般来讲重载运算符函数至少有一个参数~ 》 ⼀元运算符有⼀个参数,⼆元运算符有两个参数 》 二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数 (严格的匹配原则)
》 如果⼀个 重载运算符函数是成员函数 ,则它的 第⼀个运算对象默认传给隐式的this指针 ~ 因此 运算符重载作为成员函数时,参数会比运算对象少一个(已经有一个this指针指向的内容了~)
例:(运算符重载函数为成员函数)
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//全缺省构造函数——也可以叫默认构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//运算符重载==
//== 有两个操作对象,但是这里成员函数隐含一个this指针
//严格匹配规则,左侧运算对象传给第⼀个参数(this指针)
// 右侧运算对象传给第二个参数e
bool operator== (const Example& e)
//传参使用引用,减少拷贝次数
//const 修饰,不希望被修改
{
return _x == e._x && _y == e._y;
}
};
int main()
{
Example e1(1, 1);
Example e2(2, 2);
//运算符==重载
if (e1 == e2)//这种写法更加方便
//if (e1.operator==(e2))
//也可以像第二种这样显示写
{
cout << "e1 == e2" << endl;
}
else
{
cout << "e1 != e2" << endl;
}
return 0;
}
例:(运算符重载函数为全局函数)

当定义为全局函数就存在问题了,这是为什么呢?
很简单~因为在类里面_x和_y成员变量是私有的,所以在类外面是不可以访问的~那我们有什么方法解决呢?这里给出几种方法~
1、 成员放公有 2、 类里面提供getxxx函数 (获取成员变量的函数) 3、 友元函数 4、 重载为成员函数 (最开始的方法)
我们一个个来看:
》成员放公有
#include<iostream>
using namespace std;
class Example
{
public:
int _x;
int _y;
//全缺省构造函数——也可以叫默认构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2
//方法一:成员变量公有,类外就可以访问~
bool operator==(const Example& e1, const Example& e2)
{
return e1._x == e2._x && e1._y == e2._y;
}
int main()
{
Example e1(1, 1);
Example e2(2, 2);
//运算符==重载
if (e1 == e2)
{
cout << "e1 == e2" << endl;
}
else
{
cout << "e1 != e2" << endl;
}
return 0;
}
》类里面提供getxxx函数 (获取成员变量的函数)
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//全缺省构造函数——也可以叫默认构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//const加在后面,const修饰对象不被修改(后面会讲到)
int GetX()const
{
return _x;
}
//const加在后面,const修饰对象不被修改
int GetY()const
{
return _y;
}
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2
//方法二:类里面提供get函数
bool operator==(const Example& e1,const Example& e2)
{
return (e1.GetX() == e2.GetX())&& (e1.GetY() == e2.GetY());
}
int main()
{
Example e1(1, 1);
Example e2(2, 2);
//运算符==重载
if (e1 == e2)
{
cout << "e1 == e2" << endl;
}
else
{
cout << "e1 != e2" << endl;
}
return 0;
}
》友元函数 (这个在后面会详细讲解~这里先使用一下)
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//全缺省构造函数——也可以叫默认构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//使用友元函数,友元函数类外可以访问私有成员
friend bool operator==(const Example& e1, const Example& e2);
};
//运算符重载==(全局函数)
//== 有两个操作对象
//严格匹配规则,左侧运算对象传给第⼀个参数e1
//右侧运算对象传给第二个参数e2
//方法三:使用友元函数
//友元函数只需要在声明处加friend
bool operator==(const Example& e1, const Example& e2)
{
return e1._x == e2._x && e1._y == e2._y;
}
int main()
{
Example e1(1, 1);
Example e2(2, 2);
//运算符==重载
if (e1 == e2)
{
cout << "e1 == e2" << endl;
}
else
{
cout << "e1 != e2" << endl;
}
return 0;
}
还有一种方法就是把它设置为成员函数,前面实现过,这里就不多说啦~
我们来看看其他的注意点~
》 运算符重载后, 优先级和结合性与对应的内置类型运算符保持⼀致
》 不能通过连接语法中没有的符号来创建新的操作符 ,比如operator@
》 .* :: sizeof ? : . 以上5个运算符不能重载 ~
其他运算符都好说,.* 这个运算符我们是没有见过的,那么这个运算符有什么作用呢?我们结合代码一起来看看~
例:
#include<iostream>
using namespace std;
class A
{
public:
void func()
{
cout << "func( )" << endl;
}
};
//typedef void(*)() PF;//err 错误的重命名函数指针写法
//正确写法:名称与前面嵌套在一起
//因为是成员函数前面加A::
typedef void(A::* PF)(); //成员函数指针类型
int main()
{
// C++规定成员函数要加&才能取到函数指针
PF pf = &A::func;
//A::func到类里面找func函数
//&A::func得到它的地址——成员函数指针
A obj;//定义A类对象obj
//(*pf)();//err //想要调用成员函数,有一个this指针
// 对象调用成员函数指针时,使⽤.*运算符
//先对pf解引用得到这个函数,调用对象的func函数
(obj.*pf)();
return 0;
}
这里使用 .* 操作符就可以通过函数指针调用对象的成员函数~
》 操作符 至少有⼀个类类型参数 ,注意 不能通过运算符重载改变内置类型对象的含义 (比如不能把加变成减) 如: int operator+(int x, int y) //err 没有类类型参数
》 ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义~ 》 像Date类重载operator-就有意义,但是重载operator+就没有意义~
》 重载++运算符时,有 前置++和后置++ , 运算符重载函数名都是operator++ 》 为了更好的区分它们~所以C++规定, 后置++重载时,增加一个int形参,跟前置++构成函数重载 ,这里加int 形参只是为了好区分,没有什么十分特别的含义~
例:
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//前置++
Example& operator++()
{
cout << "前置++" << endl;
//先++再使用
//也就是自己++后返回
(*this)._x += 1;
(*this)._y += 1;
return *this;//返回使用引用减少拷贝次数
}
//后置++
Example operator++(int)//后置++重载时,增加一个int形参
{
cout << "后置++" << endl;
Example tmp = *this;
//先使用再++
(*this)._x += 1;
(*this)._y += 1;
return tmp;
//不可以引用返回,tmp是临时对象,需要拷贝一份,避免后面销毁
}
//打印
void Print()
{
cout << _x << "," << _y << endl;
}
};
int main()
{
Example e1(2, 2);
Example e2(2, 2);
//前置++
Example e3 = ++e1;
cout << "e3 = ++e1" << endl;
e3.Print();
//后置++
Example e4 = e2++;
cout << "e4 = e2++" << endl;
e4.Print();
cout << "e1" << endl;
e1.Print();
cout << "e2" << endl;
e2.Print();
return 0;
}
通过运算符重载也就达到了我们想要的效果~
接下来我们来看看<<和>>运算符重载~
一般我们打印内容可以直接使用Print函数,使用<<和>>对类类型是不太现实的~所以我们就可以进行运算符重载,直接以相应的格式输出或者输入内容~ 》 重载<<和>>时(流插入和流提取运算符),需要重载为全局函数 》 我们知道<<和>>是 二元运算符 , 有两个操作数(这里一个是cout/cin,一个是类) 》通过查询我们可以发现 cout是ostream类型,cin是istream类型~ 》注意: ostream类型和istream类型是不支持拷贝的 它们之所以内置类型可以直接使用,是因为编译器以及重载好了不同的内置类型,但是自定义类型就需要我们自己实现~


》 如果重载为成员函数,this指针默认抢占了第一个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 【 对象<<cout】,不符合我们使用习惯以及代码可读性~

》重载为全局函数把ostream/istream放到第⼀个形参位置,第二个形参位置是类类型对象
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//err
//成员函数第一个形参隐含this指针
//参数列表就是【类,cout】,不符合我们使用习惯以及代码可读性
/*void operator<<(ostream& out)
{
out << "(_x,_y)=" << "(" << _x << "," << _y << ")" << endl;
}*/
//使用友元函数
//ostream& out 只可以使用引用,ostream类不支持拷贝
friend void operator<<(ostream& out, const Example& e);
};
//类外使用友元函数
void operator<<(ostream& out, const Example& e)
{
out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
}
int main()
{
Example e1(2, 2);
Example e2(6, 6);
cout << e1;
cout << e2;
return 0;
}
但是这里还有一个问题,就是不能连续输出~这里我们就需要给它优化一下~
》我们知道<<从左向右结合,只需要给一个返回值对象也就是cout就可以了~ 》返回值对象作为下一次的左操作数
//类外使用友元函数
ostream& operator<<(ostream& out, const Example& e)
{
out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
return out;
}
这就达到了我们想要的效果~
我们来看看流提取(>>)运算符重载:
注意:(类型变为istream) istream& operator>>(istream& in, Example& e) // Example& e 不能加const,因为我们需要向里面写内容,是修改内容了的~
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
friend ostream& operator<<(ostream& out, const Example& e);
friend istream& operator>>(istream& in, Example& e);
};
//类外使用友元函数
ostream& operator<<(ostream& out, const Example& e)
{
out << "(_x,_y) = " << "(" << e._x << "," << e._y << ")" << endl;
return out;
}
istream& operator>>(istream& in, Example& e)
// Example& e 不能加const,我们需要向里面写内容,是修改了的
{
cout << "请输入x、y:" << endl;
in >> e._x >> e._y;
return in;
}
int main()
{
Example e1(2, 2);
Example e2(6, 6);
cout <<"最开始:" << e1 << e2;
cin >> e1 >> e2;
cout << "cin后:" << e1 << e2;
return 0;
}
知道了运算符重载的概念~接下来,我们来看看一个比较特殊的运算符重载,赋值运算符重载~
》在内置类型里面我们可以使用赋值运算符(=)把一个变量的值传给另外一个变量

》对于自定义类型就不可以直接使用赋值运算符(=),因为这个行为是编译器没有定义的~ 》对于自定义类型,我们就可以使用赋值运算符重载~ 》注意:赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,需要与跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个新创建的对象~
例:
#include<iostream>
using namespace std;
class Example
{
private:
int _x;
int _y;
public:
//构造函数
Example(int x = 1, int y = 1)
{
_x = x;
_y = y;
}
//e2 = e1;
void operator=(const Example& e)//第一个形参隐含this指针~
{
_x = e._x;
_y = e._y;
}
friend ostream& operator<<(ostream& out, const Example& e);
friend istream& operator>>(istream& in, Example& e);
};
》同时这里还有一个点需要优化一下~上面的代码不可以实现连续赋值~

》与<<和>>运算符类似,赋值运算符也是有返回值的,这样就可以实现连续赋值了~
//e3 = e2 = e1;
//引用返回,减少拷贝次数
Example& operator=(const Example& e)//第一个形参隐含this指针~
{
if (this != &e)//如果不是自己给自己赋值,就进行赋值操作~
{
_x = e._x;
_y = e._y;
}
return *this;//返回第一个,可以进行新的赋值
}
这样就可以实现连续赋值啦~
接下来,我们来总结一下赋值运算符重载的特点~
1. 赋值运算符重载是一个运算符重载,规定 必须重载为成员函数 (与其他的运算符有点区别) 》 赋值运算重载的 参数建议写成const 当前类类型引用 , 减少传值传参拷贝次数,同时不希望对象被修改
2. 有返回值 ,这样才可以 支持连续赋值 》 同时 返回值类型建议写成当前类类型引用,引用返回可以减少拷贝次数提高效率
3. 没有显式实现时, 编译器会自动生成一个默认赋值运算符重载 》 默认赋值运算符重载行为跟默认构造函数类似, 对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造~

比如上面的Example类注释掉赋值运算符重载,依然可以实现赋值,这就使用了编译器自动生成的 默认赋值运算符重载~
4. 》 像Date和Example这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。 》 像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我 们的需求,就需要我们实现深拷贝(对指向的资源也进行拷贝) 同时这里需要注意的是, 不能自己给自己赋值,如果自己给自己赋值,那么就会出问题~下面会结合例子来讲~ 》 像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。 》 小技巧 : 如果一个类显示实现了析构并释放资源,那么就需要我们显示写赋值运算符重载,否则就不需要~
例如栈赋值运算符重载~
//涉及资源,需要显示写赋值运算符重载
//st3 = st1
Stack& operator=(const Stack& st)
{
//避免原来的st3空间不够,释放原来的空间,开辟与st3一样大的空间
free(_a);
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);//开辟与st1一样大的空间
if (_a == nullptr)
{
perror("malloc fail");
exit(1);//开辟失败,退出程序
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);//把st1上的值拷贝给st3
_top = st._top;
_capacity = st._capacity;
return *this;//返回
}初始:

st3=st1赋值后:

这也就完成了赋值操作,还有一个点是这里需要判断是不是自己给自己赋值~
》如果不判断的话,像上面的代码就会出问题~ 因为首先就释放了原来的空间,那么它新开的空间里面已经是随机值了~

》同时避免自己给自己赋值也可以减少浪费,提高效率~
正确代码:
//涉及资源,需要显示写赋值运算符重载
//st3 = st1
Stack& operator=(const Stack& st)
{
if (this != &st)//不能自己给自己赋值
{
//避免原来的st3空间不够,释放原来的空间,开辟与st3一样大的空间
free(_a);
_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);//开辟与st1一样大的空间
if (_a == nullptr)
{
perror("malloc fail");
exit(1);//开辟失败,退出程序
}
memcpy(_a, st._a, sizeof(STDataType) * st._top);//把st1上的值拷贝给st3
_top = st._top;
_capacity = st._capacity;
}
return *this;//返回
}
这样就没有问题啦~
我们知道const可以修饰变量,那么const修饰成员函数又有什么特殊的意义呢?
》 将 const修饰的成员函数称之为const成员函数 (如果我们不希望函数内部的修改成员变量就可以加const)

比如上面的代码成员函数Print不加const就会造成权限放大~
》 const修饰成员函数放到成员函数参数列表的后面 (理解为规定) 》 const实际修饰该成员函数隐含的this指针 指向的内容 ,表明 在该成员函数中不能对类的任何成员进行修改 。 》 比如const 修饰Example类的Print成员函数, Print隐含的this指针由 Example* const this 变为 const Example* const this
例:
#include<iostream>
using namespace std;
class Example
{
private:
int _a;
int _b;
public:
Example(int x,int y)
{
_a = x;
_b = y;
}
//Print隐含的this指针由 Example* const this 变为 const Example* const this
void Print()const
//不希望函数内部修改成员变量
// 在参数列表后面加const
{
cout << "_a = " << _a << "," << "_ b = " << _b << endl;
}
};
int main()
{
Example e1(1, 1);
const Example e2(2, 2);
e1.Print();
e2.Print();//Print成员函数如果不使用const修饰,就会存在权限放大
}
这样就没有问题了~
》 取地址运算符重载 分为 普通取地址运算符重载 和 const取地址运算符重载(一个用于普通对象,一个用于const修饰的对象), 不写编译器也会自动生成一份
例:
1.使用编译器自己生成的~

2.自己显示写

》 一般这两个函数编译器自动生成的就足够我们使用,不需要去显示实现 。 》 如果一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,然后胡乱返回⼀个地址(有点小坏,但是也是为了代码安全性嘛)
例:
#include<iostream>
using namespace std;
class Example
{
private:
int _a;
int _b;
public:
Example(int x,int y)
{
_a = x;
_b = y;
}
//Print隐含的this指针由 Example* const this 变为 const Example* const this
void Print()const
//不希望函数内部修改成员变量
// 在参数列表后面加const
{
cout << "_a = " << _a << "," << "_ b = " << _b << endl;
}
//普通版本——用于普通对象
Example* operator&()
{
return this;//this指针就是当前对象地址
也可以 return 假地址
}
//const版本——用于const修饰的对象
const Example* operator&()const
//Example* operator&()const 不可以这样写,返回就权限放大了
{
return this;//this指针就是当前对象地址
//也可以 return 假地址
}
};
int main()
{
Example e1(1, 1);
const Example e2(2, 2);
e1.Print();
e2.Print();//Print成员函数如果不使用const修饰,就会存在权限放大
cout << &e1 << endl;
cout << &e2 << endl;//不显示写&运算符重载,也可以使用编译器自动生成的~
}