· CSDN的uu们,大家好。这里是C++入门的第十一讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题
目录
来看下面的代码,我们定义了一个日期类,实现了他的构造函数和拷贝构造函数。现在我们想要比较两个日期的大小,如果是你的话,你会怎么写呢?
- class Date
- {
- public:
- //构造函数
- Date(int year = 0, int month = 0, int day = 0)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- //拷贝构造函数
- Date(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- return 0;
- }
你可能会写一个成员函数,假设你写的是比较两个对象谁比较小。你可能会写出一个名为Less的函数,里面封装了两个Date类对象比较大小的逻辑。
- bool Less(const Date& d)
- {
- if (_year < d._year
- || (_year == d._year && _month < d._month)
- || (_year == d._year && _month == d._month && _day < d._day))
- {
- return true;
- }
- return false;
- }
然后你实例化出来两个对象运行代码发现并没有问题,非常nice。
但是这样做是不是有点麻烦呢?于是你想:要是可以直接这样写该多好呀!
cout << (d1 < d2) << endl;
直接这样写肯定是不行的。对于内置类型,编译器知道如何去比较,但是对于自定义类型,编译器就无从下手了!因为他不知道你定义的类型里面有哪些成员变量,比较逻辑是什么?
那我们该怎么做呢?C++祖师爷本贾尼设计出了一个叫做运算符重载的东东,能够满足你的一切幻想。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
1:不能通过连接其他符号来创建新的操作符:比如operator@。
2:重载操作符必须有一个自定义类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
3:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
4:.* :: sizeof ?: . 以上5个运算符不能重载。这个经常在笔试选择题中出现。
上面提到过,重载运算符的函数可以写在类里面,也可以写在类外面 (赋值运算符重载是例外哦!前一讲提到过赋值运算符是类的6个默认成员函数之一,你如果在类外重载赋值运算符,那么编译器就会提供默认的赋值运算符重载的函数,从而与你类外重载的赋值运算符冲突。)
好的我们现在就来重载一个小于运算符试试吧:下面的代码是在类内书写的版本:
- 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;
- }
我们回看运算符重载的特性:一个运算符有几个操作数,那么operator该运算符的形参列表就会有几个参数。我们在类里面实现的<符号的重载只有一个参数,那是因为类成员函数都有一个隐藏的this。
在我们重载了<运算符之后,d1 < d2的调用逻辑还是:d1.operator<(d2)。通过上面的汇编代码我们也能够看出来!
那么在类外重载<运算符应该怎么书写呢?大家不妨一试,谨记operator函数参数列表参数的个数和操作数的个数是一样的哦!
- 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;
- }
如果你是这么写的,那么恭喜你,写对了。但是如果在定义Date类的时候,你将成员变量全部设置为私有,你这里就会出现私有成员无法访问的报错提示。这该怎么解决呢?
方法一:直接将成员变量的访问权限修改为public。(成员变量暴露,不推荐)
方法二:提供能够获取到成员变量的函数,比如:GetYear() 等等。(太麻烦不推荐)
方法三:友元。在C++中有一个关键字:friend。他能够将一个函数或者一个类设置为另一个类的友元。设置为友元之后,这个函数或者类就可以直接访问另一个类被private修饰的成员变量和成员函数。
语法:friend + 函数或者类的声明。
例如:我们的operator<的全局函数想要访问Date类中的私有成员,就可以在Date类中将operator<声明为Date类的友元:
方法四:直接把operator<函数写在类的里面(这里的写在里面,可以是operator<函数的定义在类里面,实现在类外面)。(推荐的做法)
什么是赋值运算符重载?显然就是重载=这个运算符哇!
赋值运算符重载有什么用? 已经存在的两个对象,当我们将一个对象通过 = 运算符赋值给另一个对象时编译器会自动调用赋值运算符重载。
C++的前一讲我们知道赋值运算符重载也是类的6个默认成员函数之一。我们没有书写编译器会自动提供的,那么编译器自动提供的赋值运算符重载会干什么呢?想必经过之前的学习你已经能猜个大概了吧。编译器提供的赋值运算符重载对内置类型直接赋值,对自定义类型会调用该自定义类型的赋值运算符重载。我们来尝试写一个赋值运算符重载的函数吧:
- void operator=(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
我们可以看到重载=运算符之后,对象的赋值就算是成功了!通过汇编代码我们能够看到对象的赋值实际上就是调用了赋值运算符的重载函数!
但是 = 运算符的重载还没完!我们在写代码的时候肯定见过这样的代码吧:
- int a, b, c;
- int d = 1;
- a = b = c = d;
没错就是连续赋值的问题!我们来看看我们写的operator<能否做到连续赋值呢?
因此对于我们的 operator= 还需要修改一下我们的返回值来确保连续赋值的正确性。
- Date& operator=(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- return *this;
- }
因为我们的Date类里面的成员变量全部都是内置类型,因此,编译器默认提供的赋值运算符重载和拷贝构造就够用了!我们就不需要动手自己写了!但是如果说你定义的类里面有成员变量维护了堆区的空间,那么就需要你自己动手写拷贝构造和赋值运算符重载了!!赋值运算符重载为什么也要写呢,原因就是因为编译器默认提供的赋值运算符重载对于内置类型是直接赋值嘛!!!任何的指针都是内置类型,当我们的指针维护有堆区的空间时,直接复制就会有两个指针指向同一块堆区的空间,在对象销毁的时候就会发生二次析构的问题(前提是你正确书写了析构函数,没正确书写析构函数的话就是内存泄漏了)。
下面我们来看看赋值运算符重载与拷贝构造的区别:
拷贝构造:用一个已经存在的对象来初始化一个正在实例化的对象,是构造函数!
赋值运算符重载:已经存在两个对象,将一个对象成员变量的值赋值给另一个对象的成员变量。
直接看代码:请问下面的代码 Date d2 = d1; 调用的是赋值运算符重载还是拷贝构造呢?
- int main()
- {
- Date d1(2004, 01, 01);
- Date d2 = d1;
- }
答案是拷贝构造函数啦!当你分不清的时候,就看看拷贝构造函数与赋值运算符重载的定义!这里是用 d1 这个对象去初始化一个正在实例化的对象,当然是拷贝构造啦!
在赋值运算重载的实现里面,我们习惯加上一个判断:判断是否是自己给自己赋值,如果是的话,就不用赋值,直接结束函数即可!因为没有太大的意义嘛!
- //不会修改d的内容建议加上const
- Date& operator=(const Date& d)
- {
- if (this == &d)
- return *this;
- _year = d._year;
- _month = d._month;
- _day = d._day;
- return *this;
- }
我们重载+=这个运算符的目的就是为了能够让日期对象加上一个常数,然后计算加上该常数之后的日期时多少!该函数的原型:Date& operator+=(int day);
我们之前看到运算符重载的函数要求时必须要有一个自定义类型!这里看上去虽然没有,但是还是有一个隐藏的this呢!
+=的逻辑应该怎么写呢?因为在增加天数的过程中会涉及月份或者年份的增加,我们需要能够判断是否到达了增加月份的条件,因此我们可以封转一个函数,用于返回这个月的天数!例如GetMonthDay(int year, int month),这个函数用于返回当前月的天数。在operator+=的函数体中,我们直接让对象的_day加上传过来的形参,然后循环与当前月的实际天数作比较,如果大于当前月的实际天数,我们就让月份 + 1,同时让_day减去当前月的实际天数。直到_day小于当前月的实际天数为止!在此过程中注意到月份如果大于12则需要将年份+1,同时将月份修正为1。
- //获取一个月的实际天数, 不可以返回int& 因为 29 没法寻址
- int GetMonthDay(int year, int month)
- {
- static int daysArr[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
- if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
- {
- return 29;
- }
- else
- {
- return daysArr[month];
- }
- }
-
- //重载的 += 运算符
- Date& operator+=(int day)
- {
- _day += day;
- while (_day > GetMonthDay(_year, _month))
- {
- _day -= GetMonthDay(_year, _month);
- ++_month;
- if (_month == 13)
- {
- ++_year;
- _month = 1;
- }
- }
-
- return *this;
- }
在实现日期类的时候++运算符的重载也是很有必要的!++运算符右前置++和后置++两种。我们先来讲讲前置++:前置++是先++,然后返回++之后的对象!
实现方式很简单啊!因为我们之前就已经实现过 operator+= 了我们只需要复用这个接口就行了!
- //返回引用的目的是提高效率
- Date& operator++()
- {
- *this += 1;
- return *this;
- }
我们来看后置++应该怎么写!因为前置++和后置++的运算符相同,操作数也相同!祖师爷本贾尼就规定后置++的运算符重载需要多加一个形参以示区分。函数的原型 :Date& operator++(int),没错是不需要写形参的!这个int只用来与前置++构成函数重载的,接受形参并没有多大的意义。因此我们可以不用写形参!后置++返回的是++之前的那个对象,因此我们还需要创建一个对象来保存++之前的值,用于函数的返回值:
- //返回值不能是引用!不可返回局部对象的引用
- Date operator++(int)
- {
- Date tmp = *this;
- *this += 1;
-
- return tmp;
- }
我们可以看到在调用前置++的时候只传了一个实参,调用后置++的时候传了两个实参!
<<这是什么操作符?在C++的第二讲提到过,这个叫做流插入运算符!cout << "hello world" ;你肯定见过嘛!我们要打印Date对象,总不能也去写一个Print函数撒!太麻烦了!
我要直接cout << d1;
于是我们就需要重载流插入运算符了!
我们观察下面的图发现:cout是ostream的对象,嘿嘿传参的问题就得到了解决!
我们先来在类里面重载流插入运算符看看是否正确吧!
- ostream& operator<<(ostream& out)
- {
- out << _year << "-" << _month << "-" << _day << endl;
- return out;
- }
这样写没什么大问题,但是调用的时候就非常奇怪,因为我们的this指针位于形参的第一个,想要调用operator<<就必须让Date对象在前面,Date对象在前面确实能够调用了!但是非常不符合我们cout的使用习惯!因此我们需要将流插入的重载写在全局!
我们把流插入运算符的重载写在全局,并且让Date对象位于第二个参数,并且让返回值是一个ostream的对象!就能实现正确习惯的连续流插入了!
注意:形参不能加const,流插入是要往对象里面写东西的!你加上const就没法写东西了!
- ostream& operator<<(ostream& out, const Date& d)
- {
- out << d._year << "-" << d._month << "-" << d._day;
- return out;
- }
还有一个知识点:假设你写了一个Display()函数来打印Date对象中的成员变量! 这里仅用这个例子来讲解知识点,重载流插入更方便嘛!
你发现普通对象调用Display()没有任何问题,但是const 对象调用Display()就会出问题!这是为什么呢? 我们知道,Display()的参数中有一个隐藏的this指针,指向调用该函数的对象。当我们用普通对象调用Display()传过去的this指针是这样的:Date*;当我们用const对象调用 Display() 传过去的this指针是这样的:const Date*。而我们的Display()的形参是这样的:Date* 。显然是不能用Date* 去接受const Date* 的实参的!会发生权限的放大!
因此我们只要修改Display()的形参让他的this指针是 const Date* 就能解决这个问题了!祖师爷想来想去最后决定在成员函数后面加上const,此const 修饰 *this 代表this指向的内容不允许修改!
流插入你会写了,流提取问题应该也不大!cin是一个istream的对象!好的快去尝试写写吧!
- istream& operator>>(istream& in, Date& d)
- {
- in >> d._year >> d._month >> d._day;
- }
对象d不可以加const修饰,因为你要向里面写入内容嘛!记得加友元或者封转函数返回私有的成员变量哦!
以上实现的运算符重载并不包含所有,但是包含了大部分的运算符重载的知识。
例如:operator+(),operator-(),operator<() 等等。
重载运算符的时候能复用就尽量复用哈!比如你重载了 < 和 == 运算符,那么 > , >= , <= , != 都可以复用你重载的 < 和 == 运算符!
运算符重载并不是每一个都需要写!根据你的需求重载相对应的运算符即可!