♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥
♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥
♥♥♥我们一起努力成为更好的自己~♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥
✨✨✨✨✨✨ 个人主页✨✨✨✨✨✨❤C++专栏中比较重要的类与对象知识现在已经更新完毕❤
❤欢迎大家随时访问哦❤
类与对象——第一集
类与对象——第二集
类与对象——第三集
类与对象——第四集
类与对象——最后一集(日期类的实现)
在前面博客我们学习了类与对象中一些默认成员函数和基础知识~这一篇博客我们继续来探讨C++中的类与对象~准备好了吗~我们发车啦~??????
目录
运算符重载
运算符重载的出现
运算符重载规则
运算符重载注意点
<<和>>运算符重载
赋值运算符重载
const成员函数
取地址运算符重载
运算符重载
我们前面学习到了函数可以重载,那运算符重载又是什么概念呢?
运算符重载的出现
像 + , - ,* 这些运算符我们可以直接用于内置类型,但是如果用在类类型的对象上面的时候就不可以了,因为这个行为是编译器没有定义的,就会报错~
例:
#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指针)// 右侧运算对象传给第二个参数ebool 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//方法三:使用友元函数//友元函数只需要在声明处加friendbool 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 = st1Stack& 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 = st1Stack& 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成员函数 (如果我们不希望函数内部的修改成员变量就可以加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 thisvoid 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 thisvoid 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;//不显示写&运算符重载,也可以使用编译器自动生成的~}
♥♥♥本篇博客内容结束,期待与各位优秀程序员交流,有什么问题请私信♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
✨✨✨✨✨✨个人主页✨✨✨✨✨✨