类和对象(中)
类中的6个默认成员函数
严格意义上来说一个类中什么都没有,我们把它叫做空类;
比如:
class A
{
}
这就是一个空类,站在我们人类的角度看,这个类中确实什么都没有!
那么真的是什么都没有吗?
当然不是,注意我刚才的前提:站在人类的角度;
但是我们站在编译器的角度来看呢,这个类中其实有6个默认的成员函数!
这6个成员函数是由编译器自动生成的!
默认成员函数: 用户没有显示实现,编译器会自动生成的成员函数我们称为默认成员函数;
当然我们也可以自己显示实现,那么编译器就不会再生成对应的默认成员函数了,而其它没有被显示实现的默认成员函数则还是会由编译器自动实现!
按照这6个默认成员函数的功能,我们可以分为两两一组:
初始化和清理:构造函数和析构函数;
拷贝赋值: 拷贝构造函数和赋值运算符重载函数;
取地址重载: 主要是普通对象和const对象取地址,这两个很少会自己实现;
构造函数
构造函数的引入
首先我们知道在C++语法中是支持在结构体中定义成员函数的!
那么现在我们可以简单的定义一个栈类来玩一玩:
至此一个简单的栈类就实现出来了!为此我们可以利用这个栈类去实例化一个栈对象出来,但是事情到这才完成一半,因为我们还需要对这个栈对象进行初始化才能进行使用!
为此我们需要手动调用初始化函数(Init)来初始话这个栈,只有完成到这,我们才能在后续操作中完成进栈、压栈的操作!
但是呢?这个Init函数是需要我们手动调用的,既然是手动调用的,那么一定会有人忘记掉这个Init函数,那么后续的操作就会出现错误!同时C++作为一门高级语言,“手动调用”太降低它的B格了,高级语言嘛,肯定是更加智能的!
C++祖师爷嘞,也想到了这个问题,于是它就发明了构造函数,专门用来进行初始化的操作!
构造函数:只有在实例化对象时才会被调用!,也即是说我们在定义一个具体对象的时候,就顺便完成了对于对象的初始化工作,这个函数的调用是由编译器自动完成的!只有自动这个词才符合“高级”的感觉!
对比一下,以前初始化的操作:
很明显代码量变少了!更加符合我们“懒人”的标准了;
构造函数的特性
上面我们讲解了为什么会有构造函数,下面我们来讲解一下构造函数的定义和特性;
1、 构造函数的定义:
类名+( 参数1, 参数2, 参数3,……)
不需要写返回值类型(不是说返回值类型为void的意思,而是真的没有返回值,连void也不需要)
:
这就是Stack类的构造函数;构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
2、构造函数可以实现函数重载!
上面我们定义了一个无参的构造函数,那么接着我们也可以定义有参的构造函数:
3、构造函数的调用:对象实例化时,由编译器自动调用对应的构造函数!
构造函数作为一个特殊的成员函数,该函数的定义方式就可以看出来,那么它的调用方式也就自然于别的函数调用方式不太一样:
如果我们想要调用无参构造函数,我们可以按照下面的方式去调用:
如果我们想要调用有参构造函数的话,我们可以按照下面的方式来调用:
看到这你是不是觉得无参构造函数的调用方式有点奇怪,有点不符合常理,按照有参构造函数的调用方式来看的话,无参构造函数的调用方式应该是:
为什么我们要像Stack s1;这样写呢。这样写的话可读性反而变差了!
为什么部采取有参构造函数的调用方式呢?
现在我们从另一个函数的角度来看Stack s1();这个调用方式,s1是函数名,函数是无参数的,返回值是Stack类型!这不是妥妥的一个函数声明嘛,你看我们像这样解释的话,好像也没啥问题,这就会让编译器陷入歧义,这到底是想表示函数声明呢?还是利用无参构造函数来初始化一个对象呢?
为了区别这两种情况,C++就规定了无参函数的构造不允许带"( )";
4、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参构造函数 ,一旦用户显示定义(无论你定义的是那种类型的构造函数,不仅仅是指无参构造函数),编译器就不会自动生成了!
比如:现在我只定义了一个有参构造函数,如果我再去调用无参构造函数编译器就会报错!
他会告诉我们不存在默认构造函数!因为我们实现的是有参构造函数,这也是构造函数,编译器也就不会再生成构造函数了;
当然,我们如果对有参构造函数函数实现全缺省,也就可以调用无参构造了:
编译器看到Date d1;就会去调用默认构造函数(换句话说,编译器会去调用不需要参数的构造函数),这里的默认构造函数是指:无参构造函数、全缺省构造函数、编译器自动提供的构造函数(编译器生成的也是无参构造函数);
为此,无参构造函数与全缺省构造函数不能同时出现,不然编译器会报错:
但是呢,这样的写法语法上是没有问题的!编译器编译也能通过;如果再改成有参调用,运行也是没有问题的!
6、对于编译器自己生成的默认构造函数,该构造函数对于对象中的内置类型成员变量不进行处理,对于自定义类型的成员变量则去调用该自定义类型的默认构造函数来进行初始化!
对于默认的构造函数是这样,那么对于我们自己手动实现的构造函数呢?
首先,_t作为d1的一部分,我们在实例化d1的时候是不是就已经将_t的空间也开辟好了,也就是说顺便完成了_t的实例化,在C++中实例化对象过后,编译器就会马上自动调用合适的构造函数来初始化这片空间,那么_t这个对象也不例外啊!但是我们没法给_t对象指定参数,也就是没有机会对_t对象传递参数,让其去调用有参构造,为此编译器就只能去调用其不需要参数的构造函数(默认构造函数)来初始化_t,到这里我们才完成了Time
类对象的一系列操作,但是_t作为d1的子对象,我们只是完成了_t的实例化和初始化,d1对象本身也需要去调用自己的构造函数来初始化d1这块空间啊!但是这时候我们是可以为d1对象提供参数的,让其去调用合适的构造函数,只不过此时在再改变_t对象空间里面的内容的话,就已经不能叫做初始化了,只能叫做赋值!对于d1对象中的内置类型,才叫初始化!又由于对象的构造函数在创建和销毁期间只会被调用一次!因此在d1的构造函数内部我们无法完成对_t对象的初始化;
总结:
1、对于一个大类(类中既有内置类型、也有自定义类型),在实例化大类的时候就已经完成了子类的实例化和初始化!
2、一个大类调用构造函数时,子类会调用自己的无参构造函数,内置类型则看我们实现的构造函数是否有对内置类型初始化,有就会,没有就不会!
3、并且子类是用自己的无参构造函数来初始化的!
4、子类的初始化早于内置类型的初始化!
证明如下:
7、针对于我们使用默认构造函数时,不会初始化内置类型的行为,C++11中对此提出了解决方案:内置类型成员变量在类中声明时可以给默认值,当对象调用构造函数(不单指默认构造函数)时,如果没对某个内置类型进行初始化,就会用默认值初始化内置类型!
对于自定义类型不能给默认值!
析构函数
析构函数的概念
析构函数: 与构造函数功能相反,构造函数是起着初始化的作用,析构函数呢是起着“清理资源”的作用;在对象被销毁的时候,编译器会自动调用调用析构函数来清理资源;但是我们也可以提前调用析构函数来清理资源!也就是说只要对象所占空间没有被回收,我们就可以随便调用析构函数,它与构造函数不一样,他可以被手动调用,同时不受次数限制!
注意:析构函数不是完成对于对象本身的销毁,局部对象的销毁工作是由编译器完成的!!
举个例子,理解更深刻:
还不理解的话,可以看看这个例子:
对象就相当于你家别墅,析构函数就相当于你叫保姆,调用析构函数相当于你叫你家保姆打扫一下别墅内部卫生、清理一下别墅中的垃圾,保姆是清理别墅内的垃圾,不是连同别墅都清理了!!析构函数也是,析构函数自是去清理对象中成员变量存的垃圾值,不是清理对象本身!!!
析构函数特性
析构函数的特性也构造函数都差不多,对于析构函数的介绍我们将简洁一点:
析构函数是特殊的成员函数,其特征如下:
1、析构函数的格式: ~类名() ,如果我们不看"~"这就是构造函数的格式, "~"在C语言中是按位取反的意思,对构造函数取反就是析构函数;
2、构造函数没有参数;
3、由于构造函数没有参数,因此构造函数不能发生重载;
4、在类中,若未显示定义析构函数,编译器则会自动生成一个;如果我们显示实现了,则不会再生成!
5、对象在销毁的时候会由编译器自动调用该对象的析构函数,当然我们也可以提前调用,析构函数不像构造函数那样受到次数、位置的限制,只要对象实例化出来了,我们可以在任意地方、任意时刻调用析构函数,但是当最后对象被销毁时,编译器还是会自动调用一次析构函数!
6、对于一个类来说,如果成员变量既有内置类型,又有自定义类型,则在调用默认析构函数的时候,默认析构函数对自定义类型不做处理,对于自定义类型,则去调用自定义类型的析构函数;
上面是针对默认析构函数的,那么对于一般的析构函数是否适用?
如果是我们自己实现的析构函数的话,如果自己设置了对内置类型进行清理的语句,则编译器会对内置类型做处理,没有就不会,而对于自定义类型则会去调用它的析构函数;
7、 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
拷贝构造函数
拷贝构造函数概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1、拷贝构造函数格式:类名(const 类名& 形参名) or 类名(类名& 形参名)
注意:该只有参数是以上形式的构造函数才是拷贝构造函数!!!!!切记切记!!!不是这样格式的函数都不能叫做拷贝构造函数!!!!!!
举个具体例子:
2、拷贝构造函数的调用方式有两种:
3、若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。浅拷贝存在风险、一般情况像Stack类这种需要额外申请空间的类,就需要我们自己写拷贝构造函数,并且是用深拷贝来实现!
我们通过具体的例子来解释一下浅拷贝与深拷贝:
我们知道编译器提供的默认拷贝构造函数是由浅拷贝来实现的,我们就来用一用浅拷贝:
我们见识到了浅拷贝的危害,我们就的解决,为此深拷贝技术就被提出来了;
对于Stack类一样每个实例化出来的对象都需要,额外再申请空间的,就可以利用深拷贝来实现,深拷贝就是每个对象并不是单纯的进行值拷贝,而是对于那种需要额外开辟空间的,我们自己手动开辟一份:
就比如上面的st1与st2我们可以将st1中的_top、_capacity拷贝给st2,但是_a空间是不可以的,我们需要单独给st2开辟一块与st1所指空间性质一摸一样的空间并且相互独立的空间,为此编译器提供的默认拷贝构造函数就不顶用了,我们需要自己手动实现一下拷贝构造函数:
4、拷贝构造函数的参数有且仅有一个, 且类型必须是该类类型 对象的引用,使用传值的方式编译器直接报错,因为会引发无穷递归调用;
那么不允许使用值传递来实现拷贝构造函数呢?
假设现在我们允许使用值传递的方式来实现拷贝构造函数:
我们现在自己手动生成了拷贝构造函数,编译器就不会自动生成了哈:
在调用这个函数之前,我们是不是要给这个函数传参?
然后呢,又是值传递的方式来实现的参数传递,形参也就是实参的一份临时拷贝,那是不是,要先将实参拷贝给形参?实参拷贝给形参又会去调用拷贝构造函数,为了完成这一次的拷贝构造函数的调用,是不是又要先完成传参(记住第一次拷贝的目的是为了初始化对象!第二次拷贝的目的是为了完成参数的传递!),为了传参是不是又要调用拷贝构造函数,但是调用拷贝构造函数之前,是不是又要先完成传参,为了传参又要去调用拷贝构造函数……,你会发现陷入了无穷无尽的循环当中!我们永远也无法完成利用拷贝构造函数来初始化对象,于是编译器就禁止了这种行为!现在再来看看为什么要使用引用当形参,就是因为引用当形参不需要发生拷贝啊!
对于内置类型,编译器是允许进行值拷贝的,对于自定义类型,只能去调用对应的拷贝构造函数;
5、对于编译器自己提供的默认拷贝构造函数,对于内置类型实行值拷贝,对于自定义类型则去调用它的拷贝构造函数!
注意:
1、 拷贝构造函数典型调用场景:
使用已存在对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
2、类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
赋值运算符重载
运算符重载
我们都知道对于C++提供的操作符来说,操作符的操作数类型必须是语言的内置类型,对于自定义类型是无法使用这些操作符的!
但是,如果我们想要使用这些操作符该怎么办?
C++给我们提供了运算符重载技术,就是利用operator关键字来重载这些运算符,让我们自定义类型也能使用这些操作符;
具体怎么实现?
返回值类型 operator + 需要重载的运算符(操作数1类型,操作数2类型);
具体也是通过函数来实现的;
注意:
1、operator不能重载C++没有的操作符,比如:operator@
2、operator重载的运算符的参数来作为重载运算符的操作数;
3、operator重载的运算符可以发生函数重载;
4、.*
、::
、sizeof
、.
、? :
这五个操作符不能利用operator重载;
5、operator运算符重载的参数类型不能是内置类型,对于内置类型使用运算符,编译器直接将其转换为指令;
6、作为类成员函数时,operator的参数要比操作数数目少一个,因为this指针已经作为了operator参数的第一个形参;
具体使用:
我们也可以在全局重载一下“==”运算符,只不过这时候编译器不会再帮我们传递任何参数,因此运算符有几个操作数,我们就要老老实实写几个参数:
但是我们如果将这个函数写在全局的话,我们是无法直接访问对象里面的成员的,因为这些变量都是私有的,为了能在类外访问到类内的成员变量,C++比较常见的作法有:
1、对于每个成员变量,我们都写一个公共的函数来获取对应成员变量的值;
2、在类内将全局函数声明为该类的友元,也就是声明为该类的好朋友,是该类的好朋友了,就可以在该全局函数内部访问该类的成员变量了;
C++中呢,一般是将采用友元的做法:
利用关键字friend来声明即可:
这样我们就能在operator==全局函数内部访问Date类里面的私有成员了:
程序也就可以正常运行:
上面的用法都是隐式调用,我们也可以显示调用:
当然我们全局实现的operator==于类内实现的是可以同时存在的,应为这两种函数的参数类型是不一样的,第一个参数类型是:Date&,Date&,第二个是Date*,Date&;那么d1==d2,到底是去调用那个呢?
根据局部优先的原则,编译器先去类内部找,有没有这个函数,有就调用它,没得再去全局找;
因此当这两种类型的重载运算符同时存在时,编译器会先调用类内部的;
但是一般情况下,我们都是在类内部实现;
我们也可以再来重载一下运算符<
当然<=
、>
、>=
等都可以实现重载,但是我们可以复用上面已经实现的==
、<
来实现,比如>
:
其他重载读者可以自行实现一下;
赋值运算符重载
那么多运算符为和单独将赋值运算符,必然有其过人之处!
1、赋值运算符重载格式:
参数类型: const T&;采用引用可以提高传参效率;
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
检测是否自己给自己赋值:如果是给自己赋值的话,就没必要了,直接返回(*this)即可;使用:
2、赋值运算符只能再类域里面定义,不能在全局定义;
我们可以发现,当我们在全局就写个赋值运算符的样子,编译器就开始报错了;
那么为什么不能在全局定义呢?
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3、作为6个默认函数里面的其中之一,用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实
现吗?当然像日期类这样的类是没必要的;但是想Stack栈这类就需要自己实现,避免浅拷贝带来的危害;
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
前置++于后置++重载
同时前置++我们返回值采取的是引用,因为我们返回改变过后的*this,该对象出了这个函数还未被销毁;
后置++我们返回值采取值传递,因为我们返回的是改变之前的值,但是*this也要发生改变变,我们只能保存一份*this之前的值,tmp是局部对象出了该函数栈帧就会被销毁,因此我们不能进行引用;
综合应用(日期类的实现)
为了和前面学习的知识联系上,我们可以建立一个比较正规一点的工程:
1、首先写好需要用到的头文件;
2、为了避免命名冲突,我们可以建立一个命名空间:
namespace MySpace
{
}
3、在命名空间中定义我们的类:
同时为了后续的使用方便,我们可以将Date扩展的全局域,虽然看上去很罗嗦,但是本次目的是为了巩固前面所学的知识;
4、我们就可以在类中定义我们所需要的函数了,我们这里采用声明和定义分开;
对于日期类来说,我们是需要进行初始化的,如果使用编译器提供的构造函数的话得到的全是随机值,不是我们愿意看到的,我们为此可以定义一个有参构造:
因此,在.h文件中写好声明:
Date.cpp文件中定义:
我们这里也使用了一下缺省参数,使用缺省参数需要注意,缺省值,声明和定义只能给一处地方;
同时我们在定义时需要限定作用域,告诉编译器我们定义的是Date作用域下的成员函数;
那么需不需要实现一下析构函数呢,对于日期类来说不需要,因为她没有额外开辟空间;默认析构函数就够了;需不需要实现拷贝构造函数呢?也不太需要,Date日期类不存在深浅拷贝的问题,用编译器默认的就好了;需不需要实现赋值运算符重载?也不需要,因为Date类不存在额外资源的开辟,用编译器提供的就好了;
接着我们可以实现一个:一个日期+day天过后的日期,为此我们可以重载+
运算符,返回值类型设定为Date值传递的方式,因为我们不希望改变*this对象,而希望返回*this+day过后的对象:
因此函数参数、返回值类型、函数名我们可以这样写:
那么如何实现这个函数呢?
假设现在是2023-2-7,那么5天过后是多久的日期?
你坑定会毫不犹豫的说2023-2-12,怎么算的就是(2023 -2 -7) + 5 嘛;
那么50天过后呢?
是不是就答不上来了?
实际上我们也可以按照上面的那个思路先把50加上去,就变成了2023-2-57;
只不过这时候的时间不合法了嘛,这其实就是一个特殊的加法,进制在变而已;
我们在算一算2023-2月有28天,57已经超出了2023-2月的最高天数,需要向前进1,此时的进制是28,也就是2023-2月的天数:进1过后日期变成了:2023-3-29,这是个合法日期,可以返回了,那么如和判断当前日期是否合法?
比较一下day是不是小于等于当前年当前月的天数的,如果是,当前日期就是合法的;如果不是,当前日期就不是合法的,还需要继续进位;
但是我们如何获取当前年当前月的天数呢?我们可以写个函数GetMonthDay来获取;
上面是重载的+
运算符,也就是不改变调用对象本身的,如果我们想要写个改变对象本身,我们可以重载一下+=
运算符,代码逻辑我们也不需要重新写,我们直接复用一下+
运算符的函数就行了:
函数返回值可以采用引用;
同理前置++、后置++也是同理,可以复用+
运算符的函数;
现在我们再来实现一个计算day天之前的日期的函数,为此我们可以重载运算符-
那么具体代码思路与重载+
类似,2023-2-7 5天之前,是多久,我们毫不犹豫的回答:2023-2-2;
那么50天之前呢?
还是老样子,先减上去再说,就变成:2023-2-(-48);
这个日子肯定是不合法的,因为day已经为负数了;
那么怎样才能判定一个日期是否合法?
比较一下调整过后的日期的天数,是否大于0;
如果是:合法;
如果不是:不合法;
那么我们如何来使day从负数变成正数?
向前面的月份借1来补上负的day;
我们借的这个1是当前月的前一个月,为此我们计算的是前一个月的天数;
因此我们的代码可以这样写:
同理的-=
、前置–,后置–都可以实现了;
同理我们也算便将<
、==
、>
等关系运算符实现一下:
最后我们再来实现一下,日期-日期求相差天数的函数,我们同样可以重载运算符-
,operator重载的运算符函数是支持函数重载的:
因此函数的返回值、函数名、函数参数可以这样设计:
那么具体代码思路是怎么样的?
首先我们默认是:大日期-小日期;
然后呢根据日期跨不跨年的情况,我们可以分为两种:
1、不跨年:
2、跨年:
最后如果我们想要打印Date类的话,我们呢可以重载运算符<<
,cout也是一个对象,类型是ostream类;
我们会发现,没有"<<"运算符,我们明明实现了,我们换个姿势试试:
为什么会出现这样?因为我们在类内重载,operator<<的第一个参数是this,第二个才是out,this就作为了<<
的左参数;out就作为了<<
的右参数;
如果我们想要实现:cout<<d1;
有两种办法:
1、在ostream类内部实现,<<
重载;但是这显然是不可能的,我们不能改变ostream类里面的东西,因为这是库里面的东西,我们不能改变库;
2、使用全局函数,但是我们要声明该全局函数是该类的友元;
同时我们再来计算一下2023 2 7 100天以前的日期:
我们发现出bug了,两种方式计算出来的日期应该一样啊;
为了解决这个问题,我们可以调试:
改进:
那么同理-
运算符也应该改进一下:
再来运行看看:
总结:
头文件:
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
namespace MySpace
{
class Date
{
public:
friend ostream& operator<<(ostream&cout,const Date&d);//声明该函数是Date类的好朋友;
Date(int year = 1, int month = 1, int day = 1);//初始化日期类
static int GetMonthDay(int year,int month);//获取今年今月的天数;
Date& operator--();//1天之前的日期(前置)
Date operator--(int);1天之前的日期(后置)
Date operator+(int day);//几天过后的日期;(不改变自己)
Date& operator+=(int day);几天过后的日期;(改变自己)
Date operator-(int day);//几天之前的日期;(不改变自己)
Date& operator-=(int day);几天之前的日期;(改变自己)
unsigned int NumOfYear(const Date& d);//该日期是今年的第几天
static int GetYearDay(int year);//获取今年多少天
int operator-(const Date& d2);//日期-日期;
Date& operator++();//前置++(+1天)
Date operator++(int);//后置置++
bool operator==(const Date&d2);//判断两个日期是否相等
bool operator<(const Date& d2);//判断两个日期是否小于
bool operator>(const Date& d2);//判断两个日期是否大于
bool operator>=(const Date& d2);//判断两个日期是否大于等于
bool operator!=(const Date& d2);//判断两个日期是否不等于
bool operator<=(const Date& d2);//判断两个日期是否小于等于
void operator<<(const ostream& out);
private:
int _year;
int _month;
int _day;
};
}
using MySpace::Date;
Date.cpp:
#include"Date.h"
Date::Date(int year , int month , int day)//初始化日期类
{
assert(year>0&&(month>0&&month<13)&&(day<=GetMonthDay(year,month)&&day>0));
_year = year;
_month = month;
_day = day;
}
int Date::GetMonthDay(int year, int month)
{
int Day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 and year % 100 != 0) || (year % 400 == 0)))
return 29;
return Day[month];
}
Date Date::operator+(int day)//几天过后的日期;(不改变自己)
{
if (day < 0)
{
return *this - (-day);
}
Date tmp(*this);
tmp._day += day;
int MonthDay = 0;
while (tmp._day > (MonthDay = GetMonthDay(tmp._year, tmp._month)))
{
tmp._month++;
tmp._day -= MonthDay;
if (tmp._month > 12)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
Date& Date::operator+=(int day)几天过后的日期;(改变自己)
{
*this = *this + day;
return *this;
}
Date Date::operator-(int day)//几天之前的日期;(不改变自己)
{
if (day < 0)
{
return *this + (-day);
}
Date tmp(*this);
tmp._day -= day;
int MonthDay = 0;
while (tmp._day <=0)
{
//借1
tmp._month--;
//判断一下此时_month是否合法
if (tmp._month < 1)
{
tmp._year--;
tmp._month = 12;
}
MonthDay = GetMonthDay(tmp._year,tmp._month);
tmp._day += MonthDay;
}
return tmp;
}
Date& Date::operator-=(int day)几天之前的日期;(改变自己)
{
*this = *this - day;
return *this;
}
Date& Date::operator--()//1天之前的日期(前置)
{
return *this -= 1;
}
Date Date::operator--(int)1天之前的日期(后置)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
bool Date::operator==(const Date& d2)//判断两个日期是否相等
{
return _year == d2._year &&
_month == d2._month &&
_day == d2._day;
}
bool Date::operator<(const Date& d2)//判断两个日期是否小于
{
if (_year < d2._year)
return true;
else if (_year == d2._year && _month < d2._month)
return true;
else if (_year == d2._year && _month == d2._month && _day < d2._day)
return true;
else
return false;
}
bool Date::operator>(const Date& d2)//判断两个日期是否大于
{
return !(*this == d2 || *this < d2);
}
bool Date::operator>=(const Date& d2)//判断两个日期是否大于等于
{
return !(*this < d2);
}
bool Date::operator!=(const Date& d2)//判断两个日期是否不等于
{
return !(*this == d2);
}
bool Date::operator<=(const Date& d2)//判断两个日期是否小于等于
{
return !(*this > d2);
}
Date& Date::operator++()//前置++(+1天)
{
*this += 1;
return *this;
}
Date Date::operator++(int)//后置置++
{
Date tmp(*this);
*this += 1;
return tmp;
}
unsigned int Date::NumOfYear(const Date&d)
{
int sum = 0;
int Day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
for (int i = 1; i < d._month; i++)
{
int day = Day[i];
if (29==GetMonthDay(d._year,2))//判断当前年是否是闰年
{
day++;
}
sum += day;
}
sum += d._day;
return sum;
}
int Date::GetYearDay(int year)
{
int SumDay = 365;
if (29 == GetMonthDay(year, 2))
SumDay++;
return SumDay;
}
int Date::operator-(const Date& d2)//日期-日期;this-d2;
{
Date MaxDate =*this;
Date MinDate = d2;
int flag = 1;//区别正向、反向
if (*this < d2)
{
flag = -1;
MaxDate = d2;
MinDate = *this;
}
auto MaxDay = (int)NumOfYear(MaxDate);
auto MinDay = (int)NumOfYear(MinDate);
if (MaxDate._year == MinDate._year)//不跨年
{
return flag*(MaxDay - MinDay);
}
else//跨年
{
int sum = 0;
int RemainderDay = GetYearDay(MinDate._year);//获取当前年的总天数
int cur = MinDate._year + 1;
for (cur; cur < MaxDate._year; cur++)
sum += GetYearDay(cur);
return flag * (RemainderDay - MinDay + sum + MaxDay);
}
}
void Date::operator<<(const ostream&out)
{
cout << _year << "年" << _month << "月" <<_day << "日" << endl;
}
ostream& MySpace::operator<<(ostream& cout, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return cout;
}
const成员
将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针(既const修饰的是*this),表明在该成员函数中不能对类的任何成员进行修改。
1、const修饰的对象必须初始化,因此如果还是使用编译器提供的构造函数的话,就可能会出错,因为编译器提供的构造函数是不对内置类型进行处理的;
改进:构造函数使用全缺省或者在声明时给缺省值;
2、const修饰的对象只能调用const修饰的成员函数、静态成员函数;
究其原因还是权限的问题:const修饰的对象,取出来的地址是const*,而this的类型是:Date *const this,一个const*的地址传递给Date *const this是属于权限的放大,因为我已经明确规定了我的d1对象不能被修改,我现在就传递个this指针,就告诉可以通过*this指针去修改,这显然是不符合常理的,我们只能改变this的权限(利用const修饰*this),但是this是个隐藏参数,我们是不可能直接利用const去修饰*this的,既然直接无法修改,那就间接去修改:
编译器规定:允许我们在成员函数的这个位置+const,在这里加const就表示我们修饰的是*this,那么在这个成员函数中this的权限就为了:Date const * const this;这就保证了const修饰对象的权限的平移,自然我们就能利用const对象访问const成员函数了,其他动态成员函数,我们无法通过const对象来调用,因为这会引起权限的放大;
对于没有用const修饰的对象,调用const修饰的成员函数,属于权限的缩小,编译器是允许的,调用非const修饰的动态成员函数属于权限的平移,编译器也是允许的!;
对于const修饰对象还能访问静态成员函数的解释:是因为编译器对静态成员函数不会传递this指针,自然也就不存在权限问题;
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,用编译器自动生成的就够了;
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
比如:不想然别人会去对象的地址,我们就可以返回nullptr: