赋值运算符重载
运算符重载:
C++为了增强代码的可读性,可以对 运算符 进行重载,运算符重载 就是具有特殊函数名的函数,这个函数也具有返回值类型,函数名字和参数列表,它的返回值和参数列表的形式和普通函数类似。
比如,我们在比较内置类型大小的时候,可以使用 < , > , == 等等这些运算符来实现,那么在C++当中也希望,我们的自定义类型也能 和 内置类型一样使用这些运算符,所以他就搞了一个运算符重载, 其实就是实现一个函数来 实现原本 运算符实现的 效果,然后我们在使用 这些运算符的时候,如果是对应的自定义类型,就会调用这个函数,来直接实现结果。
这样的话,假设我们要比较两个自定义类型的大小,就不用写一个函数,然后再主函数中进行调用,这样就算是 写了注释,而且 函数名命名 好 的函数也需要花费时间来阅读,来思考这个函数到底实现了什么功能,如下所示:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
bool Dateequal(const Date& date1, const Date& date2)
{
return date1._year == date2._year
&& date1._month == date2._month
&& date1._day == date2._day;
}
int main()
{
Date d1(2020,1,1);
Date d2(2020,1,1);
bool Mybool = Dateequal(d1, d2);
cout << Mybool << endl; //1
return 0;
}
我们发现这个代码,就是实现了一个函数,然后调用这个函数,这样子,就算我们命名给了提示,他还是需要看代码和注释来了解这个函数实现了什么,那么如果我们比较内置类型,直接使用 == 这个运算符就可以实现了,他返回的也是 bool 类型的值:
int a = 10;
int b = 10;
cout << a == b << endl;
这样是不是非常的直观,我们一看就知道这个 a == b 是什么意思,那么我们也想 自定义类型也这样用,那么此时就可以使用 运算符 的重载了,运算符的重载使用了 operator 这个关键词。
语法:
返回值类型 operator操作符(参数列表)
那么上述例子,我们就可以这样来实现这个 运算符的重载:
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._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(2020,1,1);
Date d2(2020,1,1);
cout << (d1 == d2) << endl;//1
return 0;
}
如上述,要实现了之前的效果,但是可读性大大提高了。
需要注意的是:因为我们写的是运算符重载,这个运算符也是有优先级的,就像上述, " << " 流运算符的优先级是很高的,如果我们想要 得到 d1 == d2 这样的结果,输出的话,那么我们最好是加上括号。
如上述,如果我们不加括号 就会报错:
他会先运算 cout << d1 这一个表达式,那么就会出错。
当然,我们在选择重载运算符的时候,也要看看这个重载运算符实现的结果对这个类是否有意义,比如上述,日期 - 日期 计算天数,这个是有意义;但是 日期 + 日期 这个的结果就没有什么意义。
现在我们来实现,日期的比较大小 ( < ) :
bool operator< (const Date& d1, const Date& d2)
{
if (d1._year < d2._year)
{
return true;
}
else if (d1._year == d2._year && d1._month < d2._month)
{
return true;
}
else if (d1._year == d2._year && d1._month == d2._month && d1._day < d2._day)
{
return true;
}
return false;
}
int main()
{
Date d1(2020,1,1);
Date d2(2020,1,1);
//cout << (d1 == d2) << endl; //1
cout << (d1 < d2) << endl; //0
return 0;
}
我们只能重载 自定义类型的 运算符重载,如果全是是内置类型重载,这样是不行的:
运算符的重载,参数列表当中必须有一个是 自定义类型:
如上图,此时编译通过。
而且上述的参数个数也是有限定的,运算符有多少个操作数,我们重载的函数就应该有几个参数
我们上述都是直接访问Date类当中的 成员,成员的访问权限是 public的,其实成员应该是 protected 的,如果需要访问 protected 的成员,需要其他方法,比如友元解决,但是有元是突破访问限制,这样是不到万不得已不使用的,所以我们就干脆直接使用 类当中的成员函数:
也就是在类当中来定义这个函数:
bool operator< (const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
{
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
需要注意是:我们上述 函数 只使用了 一个参数,因为在类当中的 成员函数,在调用的时候会给一个this 指针,这个this 指针指向的就是当前对象指针,而我们上述也说过,重载的运算符函数的参数个数,是和对应的运算符的操作数是相等的,所以如果我们 此处像之前一样给了 两个参数,就会报错:
而上述我们在调用这个 重载运算符函数的时候,我们是直接 d1 < d2 ,这样来实现,但是其实,因为是在类当中实现的,那么我们应该像访问类当中的成员函数一样来调用这个函数,但是上述我们并没有报错,其实是 编译器会对这个进行转换,例如这个 d1 < d2 ,就被转换为 d1.operator<(d1,d2); 这样的表达式,这也符合我们对类当中成员函数的访问方式。
当然我们也可以这样来使用这个 重载的运算符函数:
d1.operator<(d2)
这样看似只传入一个参数,其实是两个参数,因为这个函数还需要传入这个 d1 对象的this 指针。
我们把上述两种方式都写出来,看看汇编代码是如何写的:
两个代码反汇编是一样的。
以下五个操作符是不能进行重载的:
- .*
- :: 域访问操作符
- sizeof 计算大小
- ?: 相当于 if
- . 成员访
总结 :
在 运算符重载当中,我们需要注意的是:
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- . * :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
- 是否要重载运算符,要看这个运算符实现效果对这个类是否有意义
赋值运算符重载
赋值运算符就是 " = " 这个运算符,他的定义方式和其他函数的定义方式是一样的,都是使用 operator 这个关键词来定义,如下例子:
void operator=(const Date& d1, const Date& d2)
{
}
需要注意的是:我们需要把 赋值运算符 和 拷贝构造函数 这两个 区别开来;拷贝构造函数的本质是构造函数,他是在主函数中有一个 对象的时候,调用拷贝构造函数,来创建一个新的对象,把其中的值拷贝过去;而赋值运算符 重载函数本质是 运算符重载,他是在主函数中本来就有两个对象,然后把 = 前面对象当中的值,赋值给第二个对象当中。
现在我们在类当中定义这个 赋值运算符重载函数:
void operator=(const Date& d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
}
Date类当中只需要 浅拷贝就能实现 赋值,向上述一样,就是最简单的赋值操作符的重载。
d1 = d2 执行之前:
d1 = d2 执行之后:
我们发现,成功赋值了。
但是这样写有一些问题:
我们在内置类型当中使用赋值运算符的时候,可以一次多次赋值:
int a = 10;
int b = 10;
int c = 10;
a = b = c = 0;
他是从右到左进行一次赋值的:
如果我们把 上述实现的 重载赋值运算符 运用在 我们实现的自定义类型当中,进行如上述的多元赋值,就会有些问题:
d4 = d3 = d1;
像这个二元赋值的表达式,在运算的时候就报错了:
二元 " = " 这个指的就是 赋值给 d4 的这个 " = " ,意思就是这个 " = " 的右值是 void 类型的,那就不能再进行赋值了,如下图:
这时因为:我们在写 赋值运算符函数的时候,给的返回值就是 void 的,那么这样就不太好,向上述的多元赋值就不行。
我们之前说过,像 i = j = 0; 对于 i 来说,他的接收的值是 j ,也就是说 j = 0 这个表达式返回值是 j,所以我们在 函数中的返回值也应该是对应的对象,我们可以使用this 指针来返回 当前对象:
Date operator=(const Date& d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
return *this;
}
上述代码能实现,但是其实上述代码还是有些问题,因为上述代码是传值返回,是解引用this 指针,来返回这个 对象,我们知道,函数返回是需要创建一个临时变量来拷贝到主函数中接收的变量的,如果这个对象很大,那么对内存和效率的消耗也很大。
所以,因为上述中的对象是在主函数中的,他的生命周期是在主函数中的,这个函数销毁之后,对象不会被销毁,那么我们就用使用 引用返回,这样就不用 在创建临时变量了。
Date& operator=(const Date& d2)
{
_year = d2._year;
_month = d2._month;
_day = d2._day;
return *this;
}
还有一种情况: d1 = d1 ,这种情况,在一般情况下是不会报错的,但是在一些极特殊的情况下就会报错,所以我们可以在函数中加一个断言,不给这样使用。
比如这样判断:
if( this != &d)
{
//执行代码
}
这样写也行:
if(*this != d)
但是上述是要写了 != 重载运算符函数才行,而且这样写代价有点大,每一次都需要调用函数去判断。
对于赋值 运算符的重载函数,如果用户没有显示定义,那么编译器会自动创建 赋值运算符重载。
这个默认的重载和 拷贝构造函数 的行为是一样的:内置类型/成员---值拷贝/浅拷贝;自定义类型拷贝----会去调用它的赋值重载函数。
像这里的Date 类是不用我们去写 赋值运算符重载函数的,因为这里只是浅拷贝,浅拷贝默认的赋值运算符重载就能帮我们实现。(浅拷贝就是值的方式逐字节拷贝)
像类似Stack 这样的,对应类的对象当中里面有 开辟空间的 ,这时候需要深拷贝,这时候就需要我们来 写了。
注意:我们之前实现的 普通运算符重载函数,既可以写在 全局中,又可以写在类当中,构成成员函数。但是这里的 赋值 运算符重载函数,就不能写成 全局的。
因为这里的 赋值运算符重载函数是一种特殊的 运算符重载函数,他是默认成员函数,而普通的运算符重载函数不是默认成员函数。赋值运算符重载函数,只能再类当中进行定义和声明,当然,如果定义和声明都是在类当中的,声明和定义是可以分离的。
如这个例子:
我们在全局定义了这个 赋值运算符重载函数,发现直接报错了。