赋值运算符重载
运算符重载:
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 这样的,对应类的对象当中里面有 开辟空间的 ,这时候需要深拷贝,这时候就需要我们来 写了。
注意:我们之前实现的 普通运算符重载函数,既可以写在 全局中,又可以写在类当中,构成成员函数。但是这里的 赋值 运算符重载函数,就不能写成 全局的。
因为这里的 赋值运算符重载函数是一种特殊的 运算符重载函数,他是默认成员函数,而普通的运算符重载函数不是默认成员函数。赋值运算符重载函数,只能再类当中进行定义和声明,当然,如果定义和声明都是在类当中的,声明和定义是可以分离的。
如这个例子:
我们在全局定义了这个 赋值运算符重载函数,发现直接报错了。
理解 << 流插入 操作符 和 >> 流提取 操作符
<< 流插入
现在我们就理解了,为什么在C++当中的 << 流插入操作符 可以自动识别 数据类型,其实就是实现了很多的 << 运算符重载函数:
如上图所示,实现了很多 << 的运算符重载函数。
但是 << 流插入操作自动识别是在库里实现的,内置类型的重载,但是自定义类型没有重载,所以,当我们要想使用 << 来打印 自定义类型的 数据的时候,需要重写 << 操作符重载函数:
例子:
实现日期类的流插入函数:
void Date::operator << (istream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
int main()
{
Date d1(1949, 10, 1);
cout << d1;
return 0;
}
上述实现是错误的,会报如下的错误:
这是因为我们在 main 函数中调用方式有问题,我们以往在调用重载运算符函数的时候,比如 d1 + d2,在编译的时候会转换成 d1.operator+(d2) 这样的形式来调用;那么上述的 cout << d1 ,我们想要转化的是这样的--- d1.operator<<(cout) ;这样的形式,但是注意的是:上述转化应该是这样的: cout.operator(d1) ; 这样的话,编译器就不能识别了,我们这样使用就可以打印了: d1 << cout 。 如下所示:
d1 << cout 才会转化为 我们想要的 d1.operator<<(cout) ;的形式。
但是 d1 << cout 这样使用很别扭,一般都是 cout << 对象,这样来实现的,因为我们在说 << 流插入操作符的时候,说的是,右边的对象流入左边 cout (终端控制台)中,如果我们想要实现像之前 cout << 内置类型 ;这样的操作的时候,我们需要先在库当中实现Date类的定义,修改库当中(ostream类)定义的 << 重载函数,因为只有在 ostream类当中,第二个参数才能是 Date。
但是,库当中的代码我们是不能修改的,所以,我们在类当中实现 << 流插入操作的重载是不可行的,因为 如上述 在 Date类当中,我们实现成员函数,默认第一个参数就是 做的是 左操作数,这就注定了,我们在使用这个 << 重载函数的时候,就非常别扭,不符合使用习惯。
所以我们把这个函数定义为全局的函数,这样我们就可以定义参数的位置了:
int Date::Getyear()
{
return _year;
}
int Date::Getmonth()
{
return _month;
}
int Date::Getday()
{
return _day;
}
void operator <<(ostream& out,const Date& d)
{
out << d.Getyear() << "年" << d.Getmonth() << "月" << d.Getday() << "日" << endl;;
}
我们在类当中实现三个函数,分别帮我们拿到 三个成员的数据。
类似这样的一种定义,这样我们在调用这个函数的时候就,可以像 cout << d1 这样的方式来调用了。
除了像上述一样用 对应 的 get 和 set 函数可以拿到 和 修改 对象当中 成员的值,在C++当中我们还可以用 友元函数来实现,我们可以在类当中的任意位置来写入下面这个代码:
这样就表示,这个函数被当做为这个类的朋友,引入这个函数做为朋友,那么就可以突破这个类当中的访问权限的限制,直接访问到类当中的 成员和 成员函数。
像上述的操作,就是友元函数的声明。
那么关于 << 流插入函数的实现还是有一些问题,我们在使用 << 流插入的时候,还会这样来使用:
cout << d1 << d2 << d3 << endl;
那么向上述 的过程是 d1 先流入 cout 然后再是 d2 ,最后是 d3,所以我们应该这个函数应该返回一个 ostream& 的值:
ostream& operator <<(ostream& out,const Date& d)
{
out << d.Getyear() << "年" << d.Getmonth() << "月" << d.Getday() << "日" << endl;;
return out;
}
>> 流提取
同样有了输入,就有输入, >> 提取流,就能帮助 对 自定义类型当中的成员进行输入:
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
总结
C++为什么要引入流这些操作符,不仅仅是因为更方便理解,我们在C当中的 printf()和scanf()等等这些输入输出的函数,只能对内置类型进行输入输出,对自定义类型不能像内置类型一样的输出,那么在C++当中充值了这个 << 和 >> 两个操作符之后,就可以对自定义的类型进行输入输出。
除了自定义类型的输入和输出,我们自定义类型当中的成员的值,有时候也是有输入和输出格式的,比如上述的日期,月就之后 1- 12 ,没有类型 13 这样的月份,那么我们在用 >> 提取流输入的时候就可以进行判断:
我们可以在 >> 和 << 重载函数中去 判断,也可以在 构造函数的中去判断,如下就是在构造函数中去判断:
Date::Date(int year, int month, int day)
{
if ((_month < 13 && _month > 0) && _day < GetMonthDate(_year, _month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法输入" << endl;
}
}