Ⅰ. 运算符重载
引入
❓什么叫运算符重载?
就是:运用函数,将现有的运算符重新定义,使其能满足各种自定义类型的运算。
回想一下,我们以前运算的对象是不是都是int、char这种内置类型?
那我们自定义的“preson”类型,想要进行加减运算,该怎么办呢?
这就需要运算符重载。
概念
运算符重载是具有特殊函数名的函数,
也具有其返回值类型、函数名及参数列表。
函数名:关键字operator后面接需要重载的运算符符号。
格式:返回值类型 operator 操作符(参数列表)
1.常用的操作符有:+、-、*、/、++、--、=(赋值)、==(判断相等)、>、<、>=、<=等
2.有几个操作数,就有几个形参。
不过,当重载成员函数时,有一个形参是隐形的,即this指针。
✨说明:
1.不能通过连接其他符号来创建新的操作符。
如:operator@
2.重载操作符必须有一个类类型的参数。
如果参数里没有类类型,那运算符重载还有啥意义。
3.用于内置类型的运算符,其含义不能改变。
如:内置的 整型 +,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数少1,
因为成员函数的第一个参数为隐藏的this。
(见下面的例子)
5.(笔试选择题常考)这5个运算符不能重载:
.* 点星运算符
: : 域运算符
sizeof
? : 条件运算符
. 点运算符
6.运算符重载写好了以后,直接用就行。编译器会自动调用函数。
Date& operator+=(int day){
…
}
d1+=100; //直接用。调用会自动完成🤣
举例
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
} //需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d) //bool operator==(Date*this,const Date& d)
{
return _year == d._year && //其实是this->_year==d._year
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 12);
Date d2(2023, 8, 12);
cout << (d2 == d1) << endl;
}
结果为:
Ⅱ. 赋值运算符重载
概念
赋值运算符重载作为类的6大成员函数之一,
负责将一个对象赋值给另一个对象。
如果我们不写,那编译器会自动生成。
格式
T& operator =(const T& 参数)
✨说明:
1.参数类型为const T&。传引用可以提高传参效率。
2.返回类型为T&。
❓你可能会疑惑:这里只要完成赋值动作的话,返回类型为void不就可以了吗?
为什么要有返回值呢?
有返回值其实是为了支持连续赋值。
如”d1=d2=d3;“ 要想连续赋值,
那d2=d3在调用完函数以后要有一个返回值,这个返回值作为右操作数,参与到d1=…中去。
如果返回void,那d1=空,无法完成连续赋值。
所以,要想连续赋值,就得有返回值。
在返回时,我们尽量使用引用返回。
因为能减少传参过程中的拷贝,效率更高。
不信我们来实验下,
通过对比 传值返回与传引用 调用拷贝构造函数 的次数,
来看 传引用究竟有没有减少拷贝!
实验组1:传值
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d) //我们自己写一个拷贝构造函数
{ //这样,它每次被调用,都会打印出来
cout << "我被调用了!" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
Date operator=(const Date& d) //传值是可以的,但是没有引用好
{ //实验结果将为我们证明这一点
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 8, 12);
Date d2(2023, 8, 12);
Date d3(2000, 1, 1);
Date d4(2020, 1, 1);
d1 = d2 =d3 = d4;
}
结果为:(这已经是被优化后的结果)
实验组2:传引用
...
Date& operator=(const Date& d) //传引用
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
...
结果为:
实验证明,传引用比传值调用拷贝构造函数的次数少,效率更高。
所以,我们能传引用的地方,就尽量传引用。
3.检测是否自己给自己赋值。
如果是”d1=d1;“那这样的赋值完全没意义。
为了更高效,我们用if语句来避免自己给自己赋值的情况。
Date& operator=(const Date& d)
{
if (this != &d) //加上判断
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
注:我们要用this去判断,不要用对象!
因为对象仅能判断值是否相等,而this能从地址判断它俩是否是同一个。
4.返回*this。
为什么可以返回*this?
我们知道,函数的局部变量是不能返回的,
因为局部变量在出了作用域就销毁了。
而这里不同,*this是 作用域在函数外面的 对象。
出作用域,对象并不会因此销毁。所以*this有效。
只能重载成成员函数
赋值运算符只能重载成成员函数,不能重载成全局函数。😥
因为如果不在类中实现,那编译器会生成一个默认的。
此时你在类外实现的全局运算符重载,就和默认的那个冲突了。
因此,赋值运算符必须定义成成员函数。
赋值or拷贝构造?
来看这个例子:这两种写法,分别是赋值还是拷贝构造?
其实都是拷贝构造!
赋值操作的是一个已存在的变量👌,而拷贝构造是定义一个新的变量。
默认赋值运算符重载
当你没有显示实现时,编译器会自动生成一个默认的赋值运算符重载,
以值的方式逐字节拷贝。
注:内置类型成员是直接赋值的,
而自定义类型成员变量需要调用 对应类的 赋值运算符重载 来完成赋值。
我们演示一下:
class Date {
public:
Date(int year = 2000, int month = 1, int day = 1)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2010,1,1);
Date d2(2023,8,12);
//我们并未实现,
d1 = d2; //但这里会自动调用 默认赋值运算符重载
return 0;
}
结果:
❓既然默认生成的已经可以完成值的拷贝了,那还需要我们自己去实现吗?
如果是像日期类这种,是不需要的,值拷贝已经足够。
但如果涉及资源管理,
如动态内存分配、指针、打开的文件等,就得深拷贝,
这时就必须要自己去实现了。
(这里的原因和拷贝构造函数那儿是贯通的。)
原因再说明一下:
如果有指针,而默认的赋值运算符重载只能浅拷贝,
并不会再开一块指针指向的空间。
这就导致了两个指针指向同一块空间,彼此相互影响。