作者:chlorine
专栏:c++专栏
你站在原地不动,就永远都是观众。
【学习目标】
- 拷贝复制——赋值运算符重载
目录
🎓运算符重载的初步认识
🌈运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字为:关键字operator后面接需要重载的运算符符号。
- 函数原型:返回值类型 operator操作符(参数列表)
//判断真假 bool operator==(参数列表); //返回类型Date 运算符= Date operator=(参数列表);
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
🌈赋值运算符重载格式 (上)
- 参数类型:const T&,传递引用可以提高传参效率
还有几个点我们后面会遇到问题提出的
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
对于operator关键字来对俩个数据之间的操作,我们首先来敲一段
🌈operator__判断俩个日期是否相等
利用operator来实现《判断俩个日期是否相等,如果相等返回1,如果不相等返回0》
bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day ==d2._day; }
这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。(友元后期会告诉)这里既然private里的成员变量无法访问。
第一种方法:给private改成public,运行成功。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void print() { cout << _year << "-" << _month << "-" << _day << endl; } //private: int _year; int _month; int _day; }; bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day ==d2._day; } int main() { Date d1(2023, 10, 5); Date d2(2023, 11, 5); cout << (d1 == d2) << endl; d1.print(); d2.print(); return 0; }
第二种方法:将重载成成员函数,在类中。
我们放进类中充当成员函数,就一定能实现嘛?看看能不能运行成功。
参数太多,可是我们就俩个对象,为什么显示参数太多呢?
——这里就提到了我们之前说的一个重要指针——this(C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。)所以我们上面实际上是三个参数,只是this对用户来说透明的,不能显示传递。
//d1==d2 //d1就相当于this,d2相当于形参列表里面一个,所以括号里面就只能有一个参数。 // bool operator==(Date* this, const Date& d2) // 这里需要注意的是,左操作数是this,指向调用函数的对象 bool operator==(const Date& x) { return _year == x._year && _month == x._month && _day == x._day; }
这里的判断俩个日期是否相等实际上就是再比较是否d1==d2?
d1就相当于this,d2相当于形参列表里面一个,所以括号里面就只能有一个参数。
这样就运行成功了。
判断俩个日期是否相等代码如下:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void print() { cout << _year << "-" << _month << "-" << _day << endl; } //d1==d2 //d1就相当于this,d2相当于形参列表里面一个,所以括号里面就只能有一个参数。 // bool operator==(Date* this, const Date& d2) // 这里需要注意的是,左操作数是this,指向调用函数的对象 bool operator==(const Date& x) { return _year == x._year && _month == x._month && _day == x._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 10, 5); Date d2(2023, 11, 5); cout << (d1 == d2) << endl; /*d1.print(); d2.print();*/ return 0; }
所以上面的代码实现了,运行没有问题
🎓运算符重载的深入认识
接下来我们了解了operator关键字的使用,我们接下来真正进入
赋值运算符重载
赋值运算符重载的内容——一个对象赋值给另一个对象。
🌈赋值运算符重载格式(下)
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 10, 5);
Date d2(2023, 11, 5);
d1 = d2;
Date d3(d1);
d1.print();
d3.print();
return 0;
}
👉拷贝构造和赋值运算符重载的区别
我们针对上面一段代码来进行解读。看看这段代码有没有一些毛病或者一些优化的地方。
Date d1(2023, 10, 5); Date d2(2023, 11, 5); d1 = d2; Date d3(d1); 这俩者有啥子区别呢?或者说 哪个是拷贝构造,哪个是赋值运算符重载呢?
- 拷贝构造:是对同类对象初始化创建对象(创建一个新对象,然后给新对象初始化)
——就如上面代码的Date d3(d1)就是拷贝构造。
- 赋值重载运算符:一个对象赋值给另一个对象(前提俩个对象都存在)
——就如上面代码的d1=d2就是赋值重载运算符。
光标对准d1=d2赋值重载运算符但是这里还没实现 ,按下fn+f10就可以看到
d2的成员变量的值赋值给了d1,继续走,创建的新的d3对象就引入了拷贝构造函数
三者都相等了,这就是运行的过程,大家可以自己敲一下来调试,进行查看。
ps:这些都是浅拷贝(值拷贝)但是日期类都是运行浅拷贝。
让我们继续来挑这段代码的毛病吧~
int i, j, k; i = j = k = 0;
对于这种赋值运算符,c语言允许不允许这样写?——允许(连续赋值)
0先赋值给k,这个表达式有个返回值,这个返回值是k,左边的操作数就是返回值,然后继续k赋值给j,j就是返回值,以此类推.......最后i的返回值就是0。
那么日期类支持嘛?
Date d4, d5; d5 = d4 = d1;
因为这里从右往左,d1赋值给d4,返回值是void,所以是无法往前走。
所以这里正确的方法是什么?
这里从右往左,d1赋值给d4,返回值应该是d4,然后d4赋值给d5
所以我们就得探究 如何让d4是返回值,而不是void?
d4=d1;
d就是d1,this就是d4的地址。this的类型是(const Date&this),所以我们的返回类型是Date.
我们需要返回d4,如何返回d4呢?
我们当初说了this不能在形参和实参的位置给,但是可以在函数内部显示给。
//赋值运算符重载 //d4=d1 Date operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
👉格式(下)
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 返回*this :要复合连续赋值的含义
a.(全局对象和静态对象)出了作用域都还在,用引用返回效率更高。
b. 局部对象出了作用域都不在了,不用引用返回.
那我们的这里的*this出了作用域还在不在?
//赋值运算符重载 //d4=d1 Date operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
——当然在啦
——因为首先我们要知道this是形参在栈区,出了作用域就会被销毁,那么这里是this嘛?是*this,*this是d4,d4的生命周期不再函数中,至少销毁了d4还在,*this只是一个中介而已。所以可以用引用返回。*this的别名是d4.
//赋值运算符重载 //d4=d1 Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
最后返回的是*this的别名那就是d4.
传值返回和传引用返回的区别:
传值返回:传值返回的是对象的拷贝,每一个operator赋值都是一次拷贝。(传值返回大多数是临时变量)
传引用返回:传引用返回的是*this的别名。
这样我们就可以连续赋值了。
- 检测是否自己给自己赋值
大家有没有想过d1=d1是怎样的呢?是编译报错还是正常运行呢?
这是成功运行的。
我们来调试看看咋样?
这样也是可以的,this和&d都是自己。
如果你不想拥有自己与自己赋值,那么就可以加一个断言
Date &operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
d1=d1
if(this!=&d) ___this是d1的地址,&d就是d的地址。如果俩者地址都相同就不用赋值了。
🌈默认赋值运算符重载
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。默认生成赋值重载跟拷贝构造行为一样;1.内置类型成员——值拷贝/浅拷贝 (Date)2.自定义类型成员会去调用他的赋值重载 (MyQueue)Stack是深拷贝,编译器自动生成的是浅拷贝。如果我们不写赋值重载函数,编译器会不会自动生成?class Date { public: Date(int year = 2003, int month = 10, int day = 5) { _year = year; _month = month; _day = day; } void print() { cout << _year << "-" << _month << "-" << _day << endl; } //拷贝构造 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 //Date& operator=(const Date& d) //{ // if (this != &d) // { // _year = d._year; // _month = d._month; // _day = d._day; // } // return *this; //} private: int _year; int _month; int _day; }; int main() { Date d1(2023, 10, 5); Date d2(2023, 11, 5); d1 = d2; d1.print(); d2.print(); return 0; }
我们给赋值运算符重载函数屏蔽调,看编译器是否会进行自动生成?
连续赋值呢?
所以默认生成的赋值运算符重载是可以实现连续对象赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,但是和拷贝构造一样,并不是所有都是值拷贝,Date和Myqueue不需要我们自己实现赋值重载,因为Date是浅拷贝(值拷贝),Myqueue是自定义类型,但是Stack是需要自己去实现的,因为它是深拷贝,而默认生成的是浅拷贝.
🌈❌重载成全局函数
你站在原地不动,就永远都是观众。