C++语言·类和对象

news2024/12/26 21:46:37

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.coloradmin.cn/o/1602204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring (四) 之配置及配置文件的操作

文章目录 1、Spring 基于注解的配置基于注解的配置引入依赖包配置实体类数据访问层业务层业务层实现测试 2、Bean和Component和Configuration的区别1 Bean:2 Component:3 Configuration:总结&#xff1a; 区别Component和Configuration区别 3、Spring读取properties配置文件准备…

Springboot框架——4.整合jdbc

1.pom.xml中导入依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency><groupId>org.springframework.boot</groupI…

操作系统part2:四种常见调度算法、进程同步和互斥机制

1. 四种常见调度算法的实现原理及优缺点。 a. 先来先服务&#xff08;FCFS&#xff09;&#xff1a; 实现原理&#xff1a;按照进程到达的先后顺序进行调度&#xff0c;先到达的进程先执行&#xff0c;直到执行完毕或阻塞。优点&#xff1a;简单易懂&#xff0c;适用于长作业…

DolphinScheduler 调度工作流报错 Host key verification failed.

文章目录 出现问题错误原因及解决方法1.SSH 免密登录配置失败、失效2.不存在该租户 建议 出现问题 在执行调度任务时&#xff0c;失败了&#xff0c;查看日志发现错误 —— Host key verification failed. 错误原因及解决方法 1.SSH 免密登录配置失败、失效 这种情况就检查…

探索分布式系统监控zabbix-------------监控Windows

扩展windows 10 server2012 server2016 server2019 监控 一、在虚拟机中安装zabbix的客户端 下载网站 Download and install Zabbix 安装系统一直托不进虚拟机中&#xff1b;因为没安装Tools组件 点击虚拟机&#xff0c;选择安装VMware Tools 查看主机名 二、在web页…

Zabbix6.0监控入门

1. Zabbix 监控系统入门简介 Zabbix 是一个基于 WEB 界面的提供分布式系统监控的企业级的开源解决方案&#xff0c;Zabbix 能监视各种网络参数&#xff0c;保证服务器系统的安全稳定的运行&#xff0c;并提供灵活的通知机制以让 SA 快速定位并解决存在的各种问题。Zabbix 分布式…

强化学习(四)基于蒙特卡罗算法 Monte-Calo 的求解

文章目录 1. 免模型学习的强化学习问题2. 利用蒙特卡洛法求解最优价值函数2.1 策略评估&#xff08;预测&#xff09;2.2 策略迭代&#xff08;控制&#xff09; 在《强化学习&#xff08;三&#xff09;基于动态规划 Dynamic Programming 的求解方法》的文末中提到&#xff0c…

rust学习(BorrowMut异常)

现象&#xff1a; 编译没有问题&#xff0c;运行时出现&#xff1a; 代码&#xff1a; pub fn do_test() {let v Arc::new(RefCell::new(100));let v1 v.try_borrow_mut().unwrap();let v2 v.try_borrow_mut().unwrap(); } 原因&#xff1a; 一个cell貌似不能同时被借用…

书生·浦语大模型实战营之OpenXLab 部署 InternLM2 实践指南

书生浦语大模型实战营之OpenXLab 部署 InternLM2 实践指南 本文档将手把手教您如何在 OpenXLab 部署一个 InternLM2-7B chat 的应用 目录 资料介绍书生浦语 InternLM介绍OpenXLab浦源平台介绍部署 InternLM2-Chat-7B demo模型准备上传模型编写代码部署应用 资料介绍 书生浦语…

Matlab求矩阵的逆,3种常用方法总结

几种求逆矩阵的方法总结&#xff0c;以Matlab语言为例 *0* 引言*1* 简单描述函数实现*2* 方法调用计算对比 0 引言 最近在使用函数库求解逆矩阵的时候发现同一个矩阵使用不同的语言、不同的求解方法会产生不同精度的结果&#xff0c;特别是阶数很高的方阵&#xff0c;一些库中的…

基于LabVIEW的CAN通信系统开发案例

基于LabVIEW的CAN通信系统开发案例 介绍了基于LabVIEW开发的CAN通信系统&#xff0c;该系统主要用于汽车行业的数据监控与分析。通过对CAN通信协议的有效应用&#xff0c;实现了车辆控制系统的高效信息交换与实时数据处理&#xff0c;从而提升了车辆性能的检测与优化能力。 项…

【MySQL数据库】 (篇一 ) 让你快速上手——新手速通版

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、如何起步&#xff1f;&#x1f3c3;‍1.创建数据库&#xff1a;2.选择数据库&#xff1a;3.删除数据库&#xff1a;4.创建表&#xff1a;5.删除表&#xff…

贝锐蒲公英自研异地组网新技术:远程视频监控,流畅度、清晰度大幅提升

在远程视频监控过程中&#xff0c;若遇到网络带宽若遇到网络波动&#xff0c;如&#xff1a;丢包、高延迟等&#xff0c;往往会导致视频流传输时发生数据丢失或延迟现象&#xff0c;从而严重影响视频画面的清晰度和流畅度。 比如&#xff1a;在公司总部集中监看远程矿山或户外水…

华为ensp中静态路由和默认路由的原理及配置

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月17日17点37分 默认路由 [Router] ip route-static <目的网络> <目的网络掩码> <下一跳地址>默认路由的作用是将无法匹配路由表中其他路由表项的…

通过WSL在阿里云上部署Django项目MySQL

前端用Vue&#xff0c;后端用Django&#xff0c; nginx&#xff0c;Mysql 参照&#xff1a; 通过WSL在阿里云上部署Vue项目_阿里云 wsl-CSDN博客 阿里云重登录 采用Ubuntu系统&#xff0c; apt update #检查是否已经安装 mysql --version systemctl status mysql apt insta…

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启动项…

【模拟】Leetcode Z 字形变换

题目讲解 6. Z 字形变换 算法讲解 class Solution { public:string convert(string s, int numRows) {if(numRows 1)return s;string ret;int step 2 * numRows - 2;int n s.size();//记录第一行for(int i 0; i < n; i step){ret s[i];}//处理接下来的行for(int i …

C++练级之路——类和对象(中二)

1、运算符重载 C为了增强代码的可读性引入了运算符重载&#xff0c;运算符重载是具有特殊函数名的函数&#xff0c;也是具有其返回值类型&#xff0c;函数名字以及参数列表&#xff0c;其返回值类型和参数列表与普通的函数类似。 函数名字为&#xff1a;关键字operator后面接需…

【前端面试3+1】14 路由跳转的方式、如何取消已经发送的ajax请求、如何按顺序发起三个ajax请求并按顺序返回、【两个数组的并集】

一、路由跳转的几种方式 1、页面跳转 使用超链接 <a> 标签&#xff1a;通过在页面中定义超链接&#xff0c;用户点击超链接后会跳转到指定的URL页面。使用重定向&#xff1a;服务器端可以通过设置HTTP响应头中的Location字段&#xff0c;将用户重定向到指定的URL页面。使…

MySQL Prepared语句(Prepared Statements)

在数据库应用中&#xff0c;很多SQL语句都会重复执行很多次&#xff0c;每次执行可能只是where条件中的变量值不同&#xff0c;但MySQL依然会解析SQL语法并生成执行计划。对于这类情况&#xff0c;可以利用prepared语句来避免重复解析SQL的开销。 文章目录 一、prepared语句优…