> 🍃 本系列为初阶C++的内容,如果感兴趣,欢迎订阅🚩
> 🎊个人主页:[小编的个人主页])小编的个人主页
> 🎀 🎉欢迎大家点赞👍收藏⭐文章
> ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍
目录
[🐼 赋值运算符重载](#🐼 赋值运算符重载)
🐼前言
在上一节,我们分享了C++默认成员函数的前3个,构造函数,析构函数,和拷贝构造函数。
如果还有疑惑的可以浏览我的上一篇文章默认成员函数-上。
这一节我们将继续探讨C++剩下的默认成员函数:
🐼赋值运算符重载
🐼运算符重载
在我们之前对内置类型做运算时,如**+ - * /** 不需要我们考虑,计算机底层就实现了😄,内置类型的运算是计算机💻是通过一系列硬件和软件解决方案来处理的。但是当运算符被用于类类型的对象时,对于这样一个复杂对象的运算,C++语言允许我们通过运算符重载的形式指定新的含义。
运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成 。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。
👇我们来看下面这个例子:
cpp重载为全局的⾯临对象访问私有成员变量的问题 有⼏种⽅法可以解决: 1、成员放公有 2、Date提供getxxx函数 3、友元函数 4、重载为成员函数 #include<iostream> using namespace std; class Date { //将运算符重载函数设置为友元 friend bool operator==(const Date& x1, const Date& x2); public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //重载为成员函数 /*bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }*/ void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //提供成员变量的接口 //int GetYear() //{ // return _year; //} //private: int _year; int _month; int _day; }; bool operator==(const Date& x1, const Date& x2) { return x1._year == x2._year && x1._month == x2._month && x1._day == x2._day; } //全局,但是成员变量变成共有 int operator-(const Date& x1, const Date& x2) { return 0; } int main() { Date d1(2024, 11, 16); Date d2(2024, 11, 15); //运算符重载的两种写法 cout << (d1 == d2) << endl; cout << (operator==(d1,d2))<<endl;// }
🌲在上述例子中,我们创建了两个日期类的对象d1,d2 ,我们想判断对象d1是否等于d2,这里重载了运算符(==) ,来帮助我们判断两个对象是否相等。我们重载成了全局的运算符重载函数,但是面临无法访问类私有成员的问题。
这里有四种解决办法:
- 提供成员变量的函数接口,如在类中实现成员函数GetYear,GetMonth等,通过这种方式可以访问到成员变量,但是这就说明外界可以访问到内部成员变量,破坏了封装。
- 将成员变量设置为共有,在上述我们就是这样做的,但这并不能保护私有成员。
- 将运算符重载函数设置为友元,这样也可以访问到私有成员变量。友元我们在后面的文章中会分享到。
- 将运算符重载函数写到类内,成为成员函数 ,这种方式是我们最常用的,没有破坏封装,提供接口给外部用户使用,通过运算符重载函数实现类的对象之间的运算。
👇下面,我们来看运算符重载函数作为成员函数的例子:
cpp#include<iostream> using namespace std; class Date { //将运算符重载函数设置为友元 friend bool operator==(const Date& x1, const Date& x2); public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //重载为成员函数 bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } //d1-d2 int operator-(const Date& d) { //内部逻辑省略 return 0; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2024, 11, 16); Date d2(2024, 11, 15); cout << (d1 == d2) << endl; cout << d1 - d2 << endl; }
🍁在这个类中我们定义了两个运算符重载函数,重载==来判断两个对象是否相等,重载**-** 来计算两个日期对象相隔几天。但是我们发现,和全局的运算符重载不同,形参只有一个,这是为啥呢😮?因为在运算符重载作为成员函数是有this指针的😏。
运算符重载特性:
- 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数(d1),右侧运算对象传给第二个参数(d2)。
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针 ,因此运算符重载作为成员函数时,参数比运算对象少⼀个。
🍁因此**-** 运算符的显示参数是int operator-(Date* const this,const Date& d),只不过成员函数this指针就隐藏了。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。也就是重载运算符是给类对象重载的,内置类型的运算符不需要我们定义。硬件就实现了
- 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
- . * :: sizeof ?: . 注意以上5个运算符不能重载。重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y),这就是不对的运算符重载
前置++和后置++
👇我们下面重载一下对象的前置++和后置++:
cpp#include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //d1++ Date operator++(int)//规定,后置++,增加一个int形参 { Date tmp = *this; (*this)._day += + 1; return tmp; } //++d Date& operator++()//不加int,好区分 { (*this)._day += +1; return *this; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2024, 11, 16); Date d2(2024, 11, 15); //运算符重载的两种写法 d2 = d1++; d1.Print(); d2.Print(); Date d3 = ++d1; d3.Print(); d1.Print(); }
🍃我们实现了对象的前置++,后置++两个运算符重载,但是他们的名字一样(名字是由operator和后面要定义的运算符共同构成),这怎么解决😮?
C++为了避免这种问题,规定:重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
这里还有一个小细节,两个运算符重载的对象,一个引用返回(引用返回可以提高效率),一个非引用返回,具体看返回后对象还是否存在。
流插入<<流提取>>
👇我们再来实现对**流插入<<流提取>>**的运算符重载,完成对对象的输入输出:
cpp#include<iostream> using namespace std; class Date { //声明为友元 friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out,const Date& d) { cout << d._year << "年" << d._month << "月" <<d. _day << "日" << endl; return out; } istream& operator>>(istream& in, Date& d) { in >> d._year >>d._month >> d._day; return in; } int main() { Date d1,d2; cin >> d1>>d2; cout << d1 << d2; }
🍃在上述例子中,我们通过重载运算符流插入<<,流提取>> ,分别对对象d1,d2,进行输入和输出。由于输出流ostream这个类的对象是cout,以便函数内部对内置类型输出,但要注意,这里out必须是第一个参数,我们刚刚提到:二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数(out),右侧运算对象传给第二个参数(d),如果交换,那么格式应该是d<<cout,这显然不符合我们的习惯。
🍃如果把流插入和流提取作为成员函数,那么第一个参数一定是this指针 ,所以,我们必须要声明在类外部,为了能突破类域访问到类中的成员变量 ,我们将这两个运算符重载函数设置为友元函数。
🌻总结:重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象
🐼 赋值运算符重载
⭐️概念:
赋值运算符重载 是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分😃,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
特点:
- 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数,这点很重要。赋值运算重载的参数建议写成const当前类类型引用,否则会传值传参会有拷贝。
- 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。拷贝构造函数无返回值。
👇举个例子:
cpp#include<iostream> using namespace std; class Date { friend ostream& operator<<(ostream& out, const Date& d); public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { cout << " Date(const Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } // 传引⽤返回减少拷⻉ // d1 = d2; Date& operator=(const Date& d) { // 不要检查⾃⼰给⾃⼰赋值的情况 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } // d1 = d2表达式的返回对象应该为d1,也就是*this return *this; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { cout << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } int main() { Date d1(2024, 7, 5); Date d2(d1); Date d3(2024, 7, 6); d1 = d3; // 需要注意这⾥是拷贝构造,不是赋值重载 // 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值 // ⽽拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象 Date d4 = d1; d1 = d2 = d3; cout << d1 << d2 << d3 << d4; return 0; }
🍃我们实现了了赋值运算符重载函数,用引用接受实参const Date& d是为了提高效率 ,减少不必要的拷贝,返回值为Date& 在减少不必要拷贝的同时,将返回值作为第一个对象,支持连续赋值。最后,我们调用刚刚重载的运算符<<来打印类对象。
在赋值操作时,对于自已给自已赋值的操作单独处理,this!=&d,确保不必要的麻烦,如在堆上开辟空间,可能会造成冲突。
运行结果:
- 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成**值拷贝/浅拷贝(**一个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
🐬如果我们不显示写赋值运算符重载,编译器对内置类型进行浅拷贝,运行结果与上图一致.
🐋总结:像Date这样的类成员变量全是内置类型且没有指向什么资源 ,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载 。对于有资源的类,如栈等 ,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自已实现深拷贝(对指向的资源也进行拷贝)。
🐟像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。这点和拷贝构造函数很相似。
🔍我们发现:赋值运算符重载很像拷贝构造函数和运算符重载的叠加,但必须为成员函数 ,运算符重载没有规定必须为成员函数,赋值运算符重载和拷贝构造函数的最大区别是:**对已经创建的对象进行赋值拷贝。**而拷贝的规则和拷贝构造函数很相似.
🐼取地址运算符重载
🐼const成员函数
⭐️概念:
将const修饰的成员函数称之为const成员函数,const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改🚫。
❄️对于上述日期类的Printf成员函数,const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this。
👇我们这里区分一下:
Date* const this 这里const修饰的是this指针,表示this指针指向的地址不能改变。
const Date* this 这里修饰的是this指针指向的内容,表示对类中成员变量函数等不能修改。
由于C++中this指针时隐示的,我们不可能修改,所以如果不想修改this指针指向的值,将const修饰成员函数放到成员函数参数列表的后面。
举例:
cpp#include<iostream> using namespace std; class Date { public: //完整写法 // void Print(const Date* const this) const //在函数列表后加上const,表示成员函数中的任何成员类不可能修改,才加,安全性更高 void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
用const修饰成员函数这样的做法安全性更好,也体现了封装的特性。
🔍我们这个也总结一下const对象和const成员函数的调用关系:
普通对象可以调用const成员函数,是一种权限的缩小。
const对象可以调用const成员函数,是一种权限的平移。
const对象不可以调用普通成员函数,是一种权限的放大。
普通对象可以调用普通的成员函数,是一种权限的平移。
const成员函数内部可以调用普通的成员函数,是一种权限的缩小。
普通成员函数内部不可以调用const成员函数,是一种权限的放大。
🐼取地址运算符重载
⭐️概念:
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
除非我们不想让对方获得类对象的地址,就可以自已实现⼀份,胡乱返回⼀个地址。
举例:
cpp#include<iostream> using namespace std; class Date { public: Date() :_year(2024) ,_month(11) ,_day(20) { } Date* operator&() { //return this; return nullptr; } const Date* operator&()const { return this; // return nullptr; } private: int _year=1; // 年 int _month=1; // ⽉ int _day=1; // ⽇ }; int main() { Date d1; const Date d2; cout << &d1<<endl; cout<<&d2<<endl; }
我们这里分别创建了一个普通对象和const修饰的对象来调用取地址运算符重载。发现对象的地址我们是可以操控的。没有特殊场景,编译器会自动调用,不用我们显示实现。
感谢你看到这里,如果觉得本篇文章对你有帮助,点赞👍收藏 ⭐️吧,你的支持就是我更新的最大动力。⛅️🌈 ☀️