C++语言·类和对象

1. 类的引入

        C语言结构体中只能定义变量,但在C++中,结构体内不仅可以定义变量,也可以定义函数,同时C++中struct的名称就可以代表类型,不用像C那样为了方便还要typedef一下。

                        

        在C++中我们管定义的结构体类型叫做类(student),管用类定义出来的变量叫对象(s1)

        引用类中的成员的方法和C语言引用结构体成员的方法一样,因为C++兼容C的,所以很多地方都是一致的

2. 类的定义

        上面我们是用struct相当于模拟了一下类,但实际上,C++中的类是用class写的,写法上就是把struct改成了class关键字

        类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数

                           

        当我们把结构体关键字struct换成类关键字class之后发现了一堆报错,不要慌,这是因为类有一个访问限定的规则:

        访问限定符有三种:1. public(公有)        2. protected(保护)        3. private(私有)

        1. public修饰的成员在类外可以被直接访问。

        2. protected和private修饰的成员在类外不能直接访问,只能通过类中的成员函数访问或修改

        3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符了,作用域到 } 即类结束

        4. class的访问权限默认为private,struct因为要兼容C所以默认为public

        所以现在大家应该明白报错的原因了吧,就是因为这个类中的访问权限都是private,所以不能在类外进行访问和修改,那么我们只需要略加改动

                        

        这时我们发现打印出来的结果并不符合预期,这时因为在GetGread函数中因为作用域的限制,参数并没有给到类中的Chinese和Math而是赋值给了参数它自己,并没有成功初始化上,聪明的大伙一定已经知道了为什么,就是局部优先原则嘛。

        为了解决这一问题,大家有个不成文的规定,就是类的成员变量前面加个下划线,以区分于函数中的参数,或者其他地方,在不破坏代码可读性的情况下解决这一问题。

                        

        那么好,现在问题就被解决了,这也是为什么我们在看很多C++代码的时候会看到这种有下划线的写法的原因。

        类定义了一个新的作用域,类的所有成员都在类的作用域当中。在类体外定义成员时,需要用作用域操作符 :: 来指明成员属于哪个类域

3. 类和对象模型

        还是上面那个栈的类,我们观察一下它在32位的状态下大小是多少。我们直接分析一下,先看成员变量,32位的指针占4个字节,两个int型变量各自占四个字节,考虑上内存对齐等要求,成员变量们总共是占12个字节。再看成员函数,它们又是如何计算的呢,是按函数指针的大小计算的吗?直接看结果:

        

        很明显,只有成员变量的大小计算进去了。

        实际上类实例化的对象,调用成员函数的地址都是一样的。实例化的意思是,当我们创建一个类的时候,里头可能包含了这个类的成员变量和操作方法,这些都被一个类封装起来了,那么这个类是不是就相当于一张图纸。然后当我们用类去创建一个变量的时候就相当于把这个图纸盖成了一栋房子,那么这个过程就叫做类的实例化。

        那么我们再说回成员函数的问题,不同被实例化的类的成员变量可能不同,所以存成员变量的信息无可厚非,但是操作方法都是一致的,所以没必要每个实例化中都存上操作方法,这太冗余了,因此成员函数的信息被存放在了公共的代码段。

        总结就是,一个类占用空间大小只考虑成员变量,当然要注意内存对齐,也要注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

        关于内存对齐的内容,请看:

C语言·自定义类型:结构体-CSDN博客文章浏览阅读929次,点赞25次,收藏22次。本节讲解了结构体的特殊声明、结构体的自引用(链表)、结构体在内存中的对齐规则、通过offsetof宏获取结构体成员的偏移量、通过#pragma修改默认对齐数、结构体传参、位段https://blog.csdn.net/atlanteep/article/details/134717687?spm=1001.2014.3001.5501

4. this指针

        我们搞一个日期类

                

        我们知道这个类中调用的函数都是同一个,那么为何初始化和打印出来的结果不同呢,这个问题的答案看似很简单,就是因为传的参数不同嘛,确实是这样的,但是我们观察Print函数,它并没有参数啊,那它是怎么依靠参数的呢?

        这就是隐形的this指针发挥了作用,实际上这段代码在编译器看来是这样的

4.1 this指针的特性

        1. this指针的类型:类类型* const ,即成员函数中,不能改变this指针

        2 只能在成员函数内部使用

        3. this指针本质上是成员函数的形参,当对象调用成员函数的时候,将对象地址作为实参传递给this形参,所以对象中不存储this指针

        4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。这条性质说人话就是this指针是隐形的,编译器会自己在函数的形参中加上,你不要自己写,写了也是报错,但是可以在类里面用,反正只要不是加在参数上就行。

        5. 因为this指针算是一个形参,所以它存储在栈中

5. 类的6个默认成员函数

        如果一个类中什么成员都没有,简称为空类。但是空类中真的什么都没有吗?事实上并不是,任何类在什么都不写时,编译器都会自动生成以下6个默认成员函数

        默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数

        1. 构造函数主要完成初始化工作

        2. 析构函数主要完成清理工作

        3. 拷贝构造是使用同类对象初始化创建对象

        4. 赋值重载主要是把一个对象赋值给另一个对象

        5. 取地址重载

        6. const对象取地址重载

        5和6这两个很少会自己实现

5.1 构造函数

        对于上面那个Data类,可以通过Init方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建的时候,就将信息设置进去呢?

        构造函数是一个特殊的成员函数,名字和类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。值得注意的是,构造函数虽然叫构造,但是它的主要责任不是开空间创建对象,而是初始化对象。

        其特征如下:

                1. 函数名与类名相同

                2. 无返回值,也就是说不需要写void

                3. 对象实例化时编译器自动调用对应的构造函数

                4. 构造函数可以重载,可以写多个构造函数,也就是说可以有多种初始化方式

        ​​​​​​​        

        这段代码就展示了构造函数的重载和如何无参和带参创建对象,要注意无参时定义对象时是不要带括号的。

5.2 析构函数

        析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,而更像是对对象中变量的清理,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

        其特征如下:

                1. 析构函数名是在类名前加上字符 ~ 

                2. 无参数,无返回值类型,写了就报错

                3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

                4. 对象声明周期结束时,C++编译器自动调用析构函数

        ​​​​​​​        

        析构函数在像malloc或者fopen的情况下一定要显式声明,像是前面那个Data类,里面的成员都是变量的情况下,当对象销毁的时候其中的成员变量也就都自动销毁了,那么这种情况下就没必要显式声明了。

        现在回顾一下前面提到的,构造函数和析构函数都是默认成员函数,也就是说,如果类中没有显式定义它们,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

        那么编译器自动生成的构造函数的规则是什么?对于内置类型成员变量,没有规定要不要做处理,结果就是随机值,当然也有的编译器会将其初始化成0;对于自定义类型成员变量才会调用它的(不用传参)构造函数,如果自己只定义了有参构造函数就会报错,但是编译器自己生成的构造函数是无参的,也就是说即使你什么都不写自定义类型成员变量也有初始化方案,只不过是编译器自己生成的。

        这个不用传参的构造函数学名是默认构造函数无参的构造函数(自己写的和编译器生成的都可以)和全缺省的构造函数都是默认构造函数,并且默认构造函数只能有一个,否则就会访问冲突,但也必须有一个,否则就会报错。

        内置类型:int char double …… 各种类型指针

        自定义类型:class struct……

        那么自动生成的构造函数意义何在?还记得之前我们用两个栈去模拟队列的场景吗?

        我们可以看到自动生成的构造函数调用了自定义类型成员变量 Stack 的构造函数,完成了Stack 类型对象的初始化,而我们不再需要自己手动初始化了,是不是很方便。

        总结一下,自定义类型其实最终也是由内置类型构成的,所以一般情况下构造函数都需要我们显示的去实现,不要像着编译器去默认生成了。只有少数的情况下可以让编译器自动生成构造函数,就比如我们上面这个 MyQueue 类,它们的成员都是自定义类型。还有一种方案可以不用写构造函数,就是用缺省值去初始化内置类型:

        这里添加了一个_size变量,但是给他了一个缺省值,这样它就在隐式构造的情况下被初始化成了1,但是当这个办法和显式构造同时存在的情况下,其值最终会被初始化成显式构造的结果,在这里也就是10。对了,我要提醒一下,显式构造和析构到要写在public中,否则怎么调用这些函数给对象初始化和清理呢。

        编译器自动生成的析构函数的规则与构造函数类似,也是内置类型不做处理,自定义类型就去调用它的析构函数。总结一下就是有资源需要清理,比如Stack,就要写析构;当没有资源需要清理,比如Date或者内置类型成员没有资源需要清理,剩下的都是自定义类型成员,比如更新了的MyQueue,就不需要写析构,让编译器自动生成就行。

5.3 拷贝构造函数

        拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

        其特征如下:

                1. 拷贝构造函数时是构造函数的一个重载形式

                2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

                3. 若为显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数按内存存储按字节完成拷贝,这种拷贝叫做浅拷贝或者值拷贝

        我们展示一下拷贝构造函数

                

        很明显,我在创建d2的时候直接用的d1的值创建的,这就是拷贝构造函数的用处。

        那么我们现在来解释一下第二条特征,为什么会引发无穷调用,其实很简单。在对象被函数传值调用的时候,首先会先去调用这个对象的拷贝构造函数,用来创建一个临时变量,那么这个对象的拷贝构造函数也是函数啊,调用的时候如果是传值调用,那么就又去调用拷贝构造函数,这样就无穷递归下去了。那么对象在传值调用的时候会先调用它的构造函数,那么我们只要避免传值调用不就可以避免无限递归了嘛,那么解决方案就是传递地址或者引用,引用肯定是最简单的,于是我们将拷贝构造函数的参数以传引用的方式替代了传值,于是就避免了无限递归。当然编译器知道这个坑,所以你要是敢在拷贝构造的时候用传值的方式做参数,它就直接报错了。

        当然,拷贝构造很灵活,拷贝的时候还可以这么写:

        ​​​​​​​        

        这括号和等于号这两种写法是等价的,但是用多了其实就感觉等号好写一点,因为跟赋值很像嘛。

        下面说一下第三点特征,默认的拷贝构造函数按内存存储按字节完成拷贝,就像memcpy一样,直接在内存上将所有值都拷贝过来了,那么是不是说我们就没有自己写拷贝构造的必要了,让编译器自己生成就行了。事实上并不是,我们看一下Stack,它的一个成员是数组的地址,那么如果让编译器默认生成的拷贝构造函数去执行的话,两个对象会共用一个数组,这就有很大问题啊,它们的push和pop分不开了,还有一个问题就是当这两个对象销毁的时候会调用两次析构函数,如果不加以判断程序就直接崩了。

        所以说这种情况下我们就要自己写拷贝构造函数,咱们自己要写深拷贝构造,就是再开一块同样大小的空间,再把值都弄进来。

        ​​​​​​​        

        总结一下,如果没有资源管理,一般情况下不需要写拷贝,如Date。如果都是自定义类型成员,内置类型没有指向资源,也用默认生成的拷贝构造就行,如MyQueue。一般情况下,不需要写析构,就不需要写拷贝构造。如果内部有指针或者一些值指向资源,需要写析构函数释放,通常就要显式写出深拷贝,如各种数据结构。

5.4 运算符重载

        C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

        函数名字为:关键字operator后面接需要重载的运算符符号

        函数原型:返回值类型operator操作符(参数列表)

        注意:

                1. 不能通过连接其他符号来创建新的操作符:比如operator@

                2. 重载操作符必须有一个类类型参数

                3. 用于内置类型的运算符,其含义不能改变,例如:+就是让两个东西相加,规范一点,不能说无理由的把 + 重载成相减的功能

                4. 作为类成员函数重载时,其形参看起来比操作数目少1,因为成员函数的第一个参数为隐藏的this

                5.  .* (调用成员函数指针)     :: (域作用限定符)     sizeof      ?: (三目选择)      . (对象.成员)

        ​​​​​​​        

        细心的朋友可能看出来了,我把成员变量给放开了,只有这样才能在全局范围的运算符重载函数中取到成员变量。那如何能在将成员变量锁住的情况下完成这个重载函数呢,有两个方案:第一是在类里面写上Get_year(),Get_month(),这种函数然后在重载函数中调用这个get函数就能得到私有空间里的成员变量了。第二给方法是把这个重载函数塞到类里面去,这个方法是C++比较推荐的。

        那么我们如果就这么放进去的话肯定会报错,因为类里面的成员函数的参数列表的第一个位置都会隐含一个this变量,这样的话这个重载函数的参数就变成了3个,这与这个2目操作符肯定是不符合的,因此就报错了,所以我们可以这么解决,删掉一个显式参数,同时显式调用这个重载函数的方案也会有所变化

        ​​​​​​​        

        如果类域和全局域里面都有这个重载函数,那么在调用的时候会优先调用类里面的。

5.4.1 赋值运算符重载

        在说赋值重载之前我们先明确一个东西,就是在拷贝构造那块也有一个类似赋值操作符的东西,同时当我们在使用赋值重载的时候也有一个赋值操作符,这两个赋值操作符一定要分清。拷贝构造那里是将一个已经存在的对象拷贝给一个将要初始化创建的对象,赋值重载是将一个已经存在的对象赋值给另一个已经存在的对象。

        因为赋值重载是一个默认成员函数,所以我们必须把它写到类里面,不能像前面那些操作符一样全局域和类域都能写。它的写法还是很简单的,很像运算符重载和拷贝构造的结合体。

        ​​​​​​​        

        当然,这个this->是可以不写的,这里我写出来是为了方便讲解。

        这段代码的原理看起来很简单,就是把形参d的内容拷贝给了this指向的对象。但是我们关注一下这个函数的返回值和返回类型。

        首先,为什么要有返回值?这是为了照顾连续赋值的情况,最右侧的对象将内容赋给左边的对象之后,返回了左边对象指针的解引用,就相当于是返回了左边的对象,这样就能拿着左边对象如此连续向左边赋值下去。

        第二函数返回值为何是类的引用,而不是类?前面我们提到过,在传递对象或者变量时,是先将对象或者变量拷贝进一个const小空间中,与变量不同的是,在拷贝对象时要调用它的拷贝构造函数。因此如果我们的返回类型是类的话,每次返回都要调用一次拷贝构造,这是有消耗的,尤其在那些要开大空间的拷贝情况下。但是如果我们的返回时类的引用,就避免了调用拷贝构造,因为引用嘛,在底层上是把这块空间的地址直接给出去,也就是说在小的const空间中存的是一块空间的地址,不存在传递对象的情况,自然也就避免了拷贝构造的消耗了。

        下面我们可以通过一段代码更清楚的看出他俩的区别:

        左边是返回类的引用,只调用了3次构造和3次析构,这说明在赋值的时候并没有任何多余消耗。右边是返回类,3次构造没有问题,但是多了两次拷贝构造,这是在传对象进小const空间时产生的,并且还多了两次析构,这是在出小const空间时产生的。

        到这里我们要提一嘴关于引用的题外话,就是函数返回值是否要选择类型引用的问题。我们知道,引用的底层其实就是指针的传递,在C++中引用可以被理解成同一块空间的不同的名称,那么作为函数返回类型的时候我们就要格外注意类似野指针的野引用问题。当返回对象是一个局部或临时对象的时候我们就不能使用引用返回,因为出了这块作用域这个对象就析构了,那么现在返回的这个引用不就是野引用了吗,当后面的栈帧覆盖上来的时候,引用指向的这块空间还可能被篡改,这个问题我们一定要注意。只有出了作用域返回对象还不会析构的情况下,才可以用引用返回,来减少拷贝构造。

        最后赋值运算符重载与其他运算符重载不同,它是一个默认成员函数。因此用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,与拷贝构造行为相同。

5.4.2 前置++和后置++

        在重载++运算符的时候明显是要区分前后置的,如果operator++()括号中什么都不写,那就是默认的一个this指针,同时因为++是单目运算符所以这样只能表示出一种++方案,就是前置++。于是在设计C++的时候祖师爷多搞了一个int形参来标志这个++是后置++。那么写出来就是这样:

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        我们在调用后置++的时候不用去刻意的写给int参数进去,正常隐式写后置编译器会自动识别并匹配后置的操作方案,当然显式写的话就要给一下这个参数了,不过随便给一个只要是int类型就行。这个int存在的意义就是为了区分前后置++,让这两个函数构成重载。

        这个+=是自己写的运算符重载

        这里面的GetMonthDay()函数就是用于拿到某年某月中有几天用的,其中考虑的闰年的存在,内核很简单就是用一个数组实现的。

        这里就要提一下了,之前我们用内置类型的时候无所谓前置后置++,只要达到目的就行了。但是现在我们在用自定义类型的时候尽量就用前置了,观察代码就能看出,前置首先是没有弄临时对象,还有返回的是一个引用,这期间至少避免了两次拷贝构造,这是日期类,拷贝构造相对来说还比较方便,但如果是深拷贝的情况下其消耗就是巨大的。

        

5.4.3 流插入<<  流提取>>

        如果我们想用cout打印一个日期对象,肯定是行不通的,因为cout只支持内置类型的流插入,那么解决办法就是重载这个流插入。

        在C++标准库中cout被定义在ostream类中,这里的形参out的位置就是将要传cout实参的位置。

        此时我们在使用的时候就会发现一个问题,当我们显式调用这个函数的时候,没什么奇怪的,但是当隐式调用的时候,因为这个函数的首个参数是一个固定的被隐藏起来的this指针,所以隐式调用起来很奇怪,不符合我们日常的使用习惯

        ​​​​​​​        ​​​​​​​        

        引起这个问题的原因无非就是调用函数时的传参顺序,那么解决办法就是将这个函数写在类外,这样我们就可以自定义传参顺序,同时还有一个连续使用流插入的问题要解决,因此我们要给这个函数搞一个返回值。

        我知道这么写又会引发那个private成员变量访问权限的问题,那么目前唯一的解决办法就是搞Get_year()如此函数,这里我们就放开一下访问权限,尝试一下连续打印的功能。

                        

        流提取的写法和流插入是高度类似的,我们直接展示代码

        

        这么写的话可能有人会误输入一些非法的日期,所以我们可以增加一个函数检查一下。

5.5 取地址重载和const对象取地址重载

        这两个成员函数很简单,我们显式写出来就是这样子

        ​​​​​​​        

        就是取地址没啥好说的,平时也没有显式定义的需求,让编译器自己生成就行。除非你不想让别人拿到这个对象的真实地址,你可以不return this,随便return个地址。

6. const成员函数

        当我们调用一个const修饰的对象的函数的时候,就会发现调用不动。

                        

        这是因为发生了权限的放大,一个const的对象是只读的,但是它的成员函数中的参数this指针却是可读可写的。

        因此C++给出的解决方法就是在函数后面加上const关键字,表示修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

        具体一点来讲,在加const成员函数之前,this指针表示成:Date* const this,这表示this指针不能改变。加了const成员函数之后,this指针表示成:const Date* const this,双const修饰这个this指针,其本身和指向的内容都不能改变。

        

7. 实现日期类

        到这里本篇已经写了8000多字了,还剩下一部分内容,我们就放到下一篇讲吧,最后我展示一个较为完善的日期类,这里面覆盖了本篇绝大多数的知识点

Date.h

#include<iostream>
using namespace std;
#include<assert.h>

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1);
	void Print()const;

	//流插入
	//	d1.operator<<(cout);
	//d1 << cout;这么打印很别扭,因为this指针必定占去了打一个参数的传参顺序
	//所以非要写同时传参顺序正常,可以写在类外,这样就能控制传参顺序了
	//void operator<<(ostream& out);

	//获取某月天数
	//直接定义在类里面默认时inline
	int GetMonthDay(int year, int month)
	{
		assert(month <= 12 && month >= 1);
		
		static int monthdayArray[13] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 100 == 0))
			return 29;

		return monthdayArray[month];
	}

	bool CheckDate();

	//比较日期
	bool operator<(const Date& d)const;
	bool operator<=(const Date& d)const;
	bool operator>(const Date& d)const;
	bool operator>=(const Date& d)const;
	bool operator==(const Date& d)const;
	bool operator!=(const Date& d)const;

	//日期+天数
	Date operator+(int)const;
	Date& operator+=(int);
	Date& operator-=(int day);
	Date operator-(int day)const;

	//日期-日期
	int operator-(const Date& d);

	//++日期
	Date& operator++();
	//d1.operator++();

	//日期++
	//为了区分前后置++,构成重载,强行增加一个int形参
	Date operator++(int);
	//d1.operator++(1);

	Date& operator--();
	Date operator--(int);


private:
	int _year;
	int _month;
	int _day;
};


//流插入
ostream& operator<<(ostream& out, const Date& d);

//流提取
istream& operator>>(istream& in, Date& d);

Date.cpp

#include"Date.h"

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;

	if (!CheckDate())
	{
		cout << "日期非法!" << endl;
	}
}

bool Date::CheckDate()
{
	if (_month < 1 || _month>12\
		|| _day<1 || _day >GetMonthDay(_year, _month))
	{
		return false;
	}
	return true;
}


void Date::Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

//流插入
//void Date::operator<<(ostream& out)
//{
//	out << _year << "年" << _month << "月" << _day << "日" << endl;
//}

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	cout << "请依次输入年 月 日:>";
	in >> d._year >> d._month >> d._day;

	if (!d.CheckDate())
	{
		cout << "日期非法!" << endl;
	}

	return in;
}


//比对大小
bool Date::operator<(const Date& d)const
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day < d._day)
				return true;
		}
	}
		return false;
}

bool Date::operator<=(const Date & d)const
{
	return *this < d || *this == d;
}

bool Date::operator>(const Date & d)const
{
	return !(*this <= d);
}

bool Date::operator>=(const Date & d)const
{
	return !(*this < d);
}

bool Date::operator==(const Date & d)const
{
	return _year == d._year\
		&& _month == d._month\
		&& _day == d._day;
}

bool Date::operator!=(const Date & d)const
{
	return !(*this == d);
}


//日期加减

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}

	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

Date Date::operator+(int day)const
{
	Date tmp(*this);
	return tmp += day;
}


Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}

		//借上个月的天数
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}


Date Date::operator-(int day)const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}




//++日期
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//日期++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}



Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}

//日期-日期
int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}
	return n * flag;
}

        .h文件中有两个用friend修饰的函数,这是友元的用法,如果一个函数用friend修饰了,那么它就可以调用类中的privet成员了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/551699.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

idea 将项目上传到gitee远程仓库具体操作

目录标题 一、新建仓库二、初始化项目三、addcommit四、配置远程仓库五、拉取远程仓库内容六、push代码到仓库 一、新建仓库 新建仓库教程 注意&#xff1a;远程仓库的初始文件不要与本地存在名字一样的文件&#xff0c;不然拉取会因为冲突而失败。可以把远程一样的初始文件删…

汇舟问卷:推荐一个挣外国人钱项目

在海外&#xff0c;问卷调查作为一种普遍的市场研究手段&#xff0c;它们能够为企业下一季度的营销策略调整提供有力的数据支撑。 每份问卷的报酬金额各不相同&#xff0c;最低为1美元&#xff0c;最高可以达到10几美元。大多数问卷的报酬在3到5美元之间。 然而&#xff0c;在…

JS-42-Node.js01-Node.js介绍

一、浏览器大战 众所周知&#xff0c;在Netscape设计出JavaScript后的短短几个月&#xff0c;JavaScript事实上已经是前端开发的唯一标准。 后来&#xff0c;微软通过IE击败了Netscape后一统桌面&#xff0c;结果几年时间&#xff0c;浏览器毫无进步。&#xff08;2001年推出…

HDFS详解(Hadoop)

Hadoop 分布式文件系统&#xff08;Hadoop Distributed File System&#xff0c;HDFS&#xff09;是 Apache Hadoop 生态系统的核心组件之一&#xff0c;它是设计用于存储大规模数据集并运行在廉价硬件上的分布式文件系统。 1. 分布式存储&#xff1a; HDFS 将文件分割成若干块…

【游戏云服务器推荐】幻兽帕鲁 我的世界 雾锁王国 饥荒联机版 英灵神殿通用云服务器 2-64G随意选 附最新价格对比

更新日期&#xff1a;4月17日&#xff08;京东云采购季持续进行&#xff09; 本文纯原创&#xff0c;侵权必究 《最新对比表》已更新在文章头部—腾讯云文档&#xff0c;文章具有时效性&#xff0c;请以腾讯文档为准&#xff01; 【腾讯文档实时更新】2024年-幻兽帕鲁服务器专…

李飞飞团队发布《2024年人工智能指数报告》,预测人工智能未来发展趋势

昨天&#xff0c;斯坦福大学 Human-Center Artificial Intelligence (HAI)研究中心发布了《2024年人工智能指数报告》。 由斯坦福大学发起的人工智能指数&#xff08;AI Index&#xff09;是一个追踪 AI 动态和进展的非营利性项目&#xff0c;旨在全面研究 AI 行业状况&#xf…

不同质量图在卡尔曼滤波相位解缠中应用探讨

文献来源&#xff1a;不同质量图在卡尔曼滤波相位解缠中应用探讨 闫 满&#xff0c;郭春华 测绘科学技术, 2019, 7(2), 65-73 卡尔曼滤波将相位解缠转化为状态估计问题&#xff0c;实现相位解缠与噪声消除的一并处理。通过建立相位的动 态方程和观测方程来求解真实相位&#x…

The O-one:开源语言模型计算机的革命

在人工智能的浪潮中&#xff0c;The O-one作为一个创新的开源项目&#xff0c;正以其独特的功能和开放性吸引着全球开发者和科技爱好者的目光。这个项目不仅仅是一个简单的语言模型&#xff0c;它是一个能够通过语音交互与计算机进行对话的智能系统&#xff0c;极大地提升了人机…

Java面试题笔记(持续更新)

Java基础 java中的Math.round(-1.5)等于多少&#xff1f; Math的round方法是四舍五入,如果参数是负数,则往大的数如,Math.round(-1.5)-1&#xff0c;如果是Math.round(1.5)则结果为2 JDK和JRE的区别&#xff1f; JDK 是 Java Development ToolKit 的简称&#xff0c;也就是…

Java——代码块

目录 一.代码块概念以及分类 二.普通代码块 三.构造代码块 四.静态代码块 一.代码块概念以及分类 使用 {} 定义的一段代码称为代码块。根据代码块定义的位置以及关键字&#xff0c;又可分为以下四种&#xff1a; 普通代码块构造块静态块同步代码块&#xff08;后续讲解多…

MySQL数据库第二天

如何授权和撤销 已经给客户授权&#xff1a; GRANT all on *.* to "用户名""获取IP地址" identified by "密码" 如果想撤销可以使用&#xff1a; revoke all on 数据库.表 form "用户名""获取的IP地址" 补充&#xff1…

基于云的内部威胁该如何应对,基础设施策略该如何部署

德迅云安全发现云的内部威胁对基于云的基础设施的安全性和完整性构成威胁&#xff0c;因此需要采取主动的风险缓解策略。组织可以通过在云中采用访问控制、用户监控、加密、员工培训和事件响应协议的组合来增强对内部威胁的防御能力。 在当今瞬息万变的网络安全世界中&#xff…

安卓官方例程

https://learn.microsoft.com/zh-cn/shows/connecton-demand/202?sourcerecommendations https://learn.microsoft.com/zh-cn/visualstudio/cross-platform/cross-platform-mobile-development-in-visual-studio?viewvs-2022 https://learn.microsoft.com/zh-cn/shows/xamari…

短信防刷之滑动验证码

前言&#xff1a;最近想写一个滑动验证码&#xff0c;前台的样式虽然很好看&#xff0c;但是并不安全&#xff0c;网上也都是一些demo&#xff0c;不是前后台分离的&#xff0c;然后就自己查资料&#xff0c;自己来完成了 滑动验证码 一、为什么要使用滑动验证码 首先&#x…

洁净区气流流型测试及拍摄注意事项-北京中邦兴业解读

洁净区气流流型测试在众多确认和验证项目中虽然看似微不足道&#xff0c;但其重要性却不容忽视。要做好气流流型测试&#xff0c;绝非易事&#xff0c;它要求精细的操作和深入的专业知识。毫不夸张地说&#xff0c;即便是对影视圈的大牌导演而言&#xff0c;若是不了解气流流型…

MySQL死锁与死锁检测

一、什么是MySQL死锁 MySQL中死锁是指两个或多个事务在互相等待对方释放资源&#xff0c;导致无法继续执行的情况。 MySQL系统中当两个或多个事务在并发执行时&#xff0c;就可能会遇到每项事务都持有某些资源同时又请求其他事务持有的资源&#xff0c;从而形成事务之间循环等…

npm命令卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动

1.现象 执行npm install命令时&#xff0c;没有报错&#xff0c;卡在reify:eslint: timing reifyNode:node_modules/webpack Completed in 475ms不动 2.解决办法 &#xff08;1&#xff09;更换淘宝镜像源 原淘宝 npm 域名http://npm.taobao.org 和 http://registry.npm.ta…

springboot 人大金仓 kingbase-备份还原,命令中带密码,支持window和linux

命令带密码参考 Java代码实现国产人大金仓数据库备份还原需求-CSDN博客文章浏览阅读818次&#xff0c;点赞16次&#xff0c;收藏12次。本人在一次项目中&#xff0c;遇到了需要在系统管理中提供给用户备份还原系统数据的功能&#xff0c;由于项目特殊性&#xff0c;项目底层数…

Day:007(3) | Python爬虫:高效数据抓取的编程技术(scrapy框架使用)

Scrapy 保存数据案例-小说保存 spider import scrapyclass XiaoshuoSpiderSpider(scrapy.Spider):name xiaoshuo_spiderallowed_domains [zy200.com]url http://www.zy200.com/5/5943/start_urls [url 11667352.html]def parse(self, response):info response.xpath(&qu…

react v18 项目初始化

按照以下命令进行傻瓜式操作即可&#xff1a; 全局安装脚手架工具&#xff1a; npm install -g create-react-app创建项目my-react-app&#xff1a; create-react-app my-react-app安装 antd: yarn add antd安装 react-router-dom&#xff1a; yarn add react-router-dom启动项…
最新文章