C++---类和对象(中)
C++---类和对象(上)
类和对象上,我们把类怎么定义,怎么访问等等都讲的差不多了,那类呢,还有一些其他的东西。
1.类的默认成员函数
什么是默认成员函数
我们不写,编译器会自动生成的就是好默认成员函数
默认成员函数总共有6个
我们重点学前4个
初始化和清理
构造函数
是啥
构造函数是一个特殊的成员函数。它虽然名字叫构造,但是它不是用来创建对象的。
为什么它不是创造对象呢?
比如说我在这儿定义一个对象。
这个对象的空间不是由我们去开的。它是属于一个局部对象。
局部对象他就在这个栈帧里面,编译的时候就计算好了,开栈帧的时候就把它开好了。
如果它是动态开辟的,那是调了一个malloc函数去完成的,它有单独的函数(当然C++里面是调一个叫new的函数来完成的)。
如果是全局的,静态的,这些也一样,都是系统开的。
所以对象开空间不归我们管。
它其实是用来初始化对象的(对象实例化的时候完成初始化)。再简单一点理解就是我们以前写的Init函数的功能。
也就是说,C++里面期望用构造函数替代Init函数。
那它凭什么替代Init函数呢?因为它比Init函数更方便。
特点
构造函数有如下特点:
1. 函数名与类名相同
2. ⽆返回值(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会⾃动调⽤对应的构造函数(相比Init函数最大的特点)
4. 构造函数可以重载
也就是说,它有不同的参数,我们可以定义多种初始化的方式。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显式定义编译器将不再⽣成
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
最后一点,在这个地方得提一点点小概念
C++把类型分成内置类型(基本类型)和⾃定义类型。
内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等 。
⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。
总结
大多数情况,构造函数都需要我们自己去实现,少数情况类似MyQueue且Stack有默认构造时,MyQueue自动生成就可以用。应写尽写。
构造函数怎么写呢?
首先,第一个构造函数是这么写的。
然后它会自动调用
这是啥意思呢?
以前你写Init函数,你是不是在这儿定义一个对象要调用显式的,再用d1去调用一下Init?
万一你忘记调用了,那是不是就没有初始化?
现在,这个地方不需要显式的调用,它会自动调用。
它的调用也和我们之前的Init函数不一样。
调用无参的不能这么写,这么写编译会报错。
这个对象是没有定义出来的。
为什么?这个东西和函数声明区分不开,所以规定不能这么写。
第一个我们定义了一个无参的,我们说构造函数可以重载。
我们再来第二个
我们可以定义一个带参的。
还可以构造一个全缺省的
这个全缺省的和这个无参的,能不能同时存在啊?
不能(我们之前说过了),会产生调用歧义。
但是换一个角度,写了全缺省了,还需要写这个无参的吗?不需要。
我们啥都不写,编译器是不是会生成一个默认的无参构造?
那我们不写的时候,这个默认的无参构造,它会干什么事情呢?
它对内置类型成员变量的初始化没有要求。内置类型到底初不初始化,不确定,看编译器,C++标准没有规定。
早期的编译器都不初始化,建议把它当成内置类型,不处理。
对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数(说明默认生成的构造函数不行,对于这个自定义类型成员没有完成初始化),那么就会报错。
报错之后怎么办呢?就只能我们自己显式去初始化。我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
析构函数
是啥
析构函数和构造函数功能相反。
它不是完成对对象本身的销毁。
这个MyQueue和这个栈,这些对象的空间开辟是编译的时候系统就开好的。
它们在这儿开空间的时候是编译器编译的时候算好它们,建立main函数栈帧,一把就开好了。
那他们对象本身的空间也是在这个函数结束了以后栈帧销毁,一把是不是就销毁了?
它完成的是对象中的资源清理释放工作。它类似于我们的Destory。
严格来说,有些类是不需要析构函数的。
为什么呢?
因为没有资源清理就不需要析构。
比如说我们的Date类,它就不需要析构。
但是我们尝试写一写,带大家看一下。
我们来看一下
构造
析构
如果有多个对象,先析构谁呢?
后定义的先析构
栈帧这个东西,和数据结构那个栈的性质还是一样,它要求后进先出。所以这儿是st2先析构。
特点
1. 析构函数名是在类名前加上字符 ~
~ 这个符号是不是我们位运算学的按位取反啊?
构造是初始化,析构的功能和它是相反的,所以它就在类名前面加个~。意思是说,它和构造函数的功能是相反的
2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
MyQueue需不需要显式写析构?-----不需要
为什么不需要?
我们不写Myqueue的析构,自动生成的析构函数会去调用两个栈的析构。
如果我在这儿写一个这,会不会内存泄漏呢?
不会
所以自定义类型成员,你不写,默认生成的会去调它的析构;你写了,对于自定义类型成员,你也不需要显式析构,祖师爷就生怕有人在这个地方胡搞
显式写析构,也会⾃动调⽤Stack的析构
那我要是不写这个呢?
那就内存泄漏了
7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构。
构造函数和析构函数最大的特点
自动调用
拷贝复制
拷贝构造函数
拷贝构造它是一个特殊的构造。它这个构造的要求是:
第一个参数是自身类型的引用
为什么第一个参数必须得是类类型的引用呢?
首先我们来看它如下的一些特点
那它的基本特点呢,构造函数有的,它肯定都有
1.拷贝构造函数是构造函数的一个重载
2.拷贝构造的第一个参数必须是自身类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用
为什么它第一个参数必须是自己这个类类型呢?
这个很好理解
因为它是完成当前对象的一个拷贝嘛。
我们说拷贝构造,构造这个地方所想表达的一个意思是什么呢,想表达的意思其实 是初始化,它是想拿自己这个类型去初始化自己
我们用date类来理解一下
我们首先来看这样的几个方面
我想拷贝一下当前这个对象,怎么拷贝呢?
我们平时创建一个日期我们肯定是用年月日初始化是不是?
但是,我有没有可能用同类型的对象去初始化?
是不是可以啊?
那这个地方调的就是一个普通构造
这个地方调的这个构造,我们就把它叫做拷贝构造
那拷贝构造必须得怎么写呢?
必须这么写
它是一个构造函数,所以函数名和类名相同。
参数
如果还有剩下参数,你必须得有缺省值
不过一般只有一个参数
我们的第一个参数必须是自身类类型对象的引用,使用传值方式编译器 直接报错
使用传值方式编译器直接报错
那我们来看一下,为什么必须是引用呢?
得换一个样例来讲一讲
首先你得看明白第一个问题:C++规定函数的传值传参要调用拷贝 构造
C语言没有这样的规定嘛
C语言整型的话就按四个字节,一个字节一个字节的拷贝 过去
结构体传就是直接按字节拷贝过去了
你现在是不是要调用Func1啊,但是要调用Func1这个函数你 是不是要先传参啊?
以前的传参就是直接传值拷贝嘛
但现在要调用拷贝构造
所以我按F11不是走进Func1,而是走到拷贝构造去了
走完拷贝构造又回来了
回来的意思是什么?
因为我要调用这个函数得先传参啊,传参是不是形成了一 个拷贝构造啊?拷贝构造完了以后,传参完成了,我再按F11
这本质相当于是不是两个函数调用啊?
这个时候再来理解:如果我们在这个地方用传值,是不是会形成所 谓的无穷递归
刚才的路径是这样的
灰色的虚线是正确的路径
那我用一个引用,这个时候传参还会不会形成一个拷贝构 造?
不会
建议加上const
这样可以保护形参不被改变
做个简单的比方
假设,我想写这样一个逻辑
但是我不小心写错了
这个时候会发生什么?
我d1本来是去初始化d2的,结果我d1自己变成随机值了(自己被改 了)
所以,传引用的时候,如果你不想改变实参,那就把const死死的加 上
再换一个角度
我们以后对于自定义类型传参的时候,还建议去用传值传参 吗?
是不是不建议啊?
因为传值传参调用拷贝构造,这是不是太费劲了?所以我们建 议传引用
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
这个部分跟构造函数不一样了
构造函数对于内置类型初始化是不确定的
析构函数对于内置类型是不是不处理啊?
拷贝构造内置类型它也管
⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉
理解一下值拷贝/浅拷贝
比如说,你是一个日期类,你有年月日,值拷贝/浅拷贝指的是一个字节一个字节 的拷贝,就像memcpy拷贝一样
假如d2要拷贝d1。年月日,如果按内存对齐算好是不是12个字节啊?,我把你一 个字节一个字节的拷贝过去。
那日期类型是不是不需要写拷贝构造啊?
我不写,它也能完成值拷贝
自动生成完成值拷贝,一个字节一个字节完成拷贝
这个东西某种程度上也是为了兼容C语言
C语言结构体传参会不会完成拷贝?
C语言是不是没有拷贝构造这样的说法?但是大家想一想一个问题,C语言传值传 参的时候会不会完成拷贝(对于结构体)?
内置类型肯定是完成拷贝嘛,比如你一个整型是不是拷贝过去了?
结构体也会拷
它其实完成的跟这儿一样,只不过他是一个字节一个字节的拷,构造函数是 一个变量一个变量的拷贝
对于自定义类型,会去调用它的拷贝构造
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。
这个栈是不是都是内置类型啊?
那按我们之前写的,我有没有写拷贝构造?我不写;我没写拷贝构造会不会完成拷贝?会
我们来看一下,又没有完成拷贝?完成了
但是,程序崩了
如果我们仔细观察这里的对象,其实我们已经发现了一些问题
栈 这个类,和我们前面讲的类有所不同,因为日期类的内置类型没有指向什么资源,栈这个类是不是指向了单独的类空间的资源的啊?
那我有12个字节,你也有12个字节,那我把我的12个字节按值拷贝,拷贝给给你,有没有问题?
是有问题的
我指向一段空间,这一段空间是0x014c94b8
那你在这个地方,直接按字节拷贝,也指向它
这个时候会导致什么问题啊?
析构的时候,这段空间是不是析构两次啊?
那一块儿空间能不能析构两次呢?不能,所以程序崩溃
问题就出来了
所以,对于栈这样的类,拷贝构造得我们自己写
对于栈这样的类,浅拷贝(值拷贝)是不是不行啊?得我们自己写
那我们自己得怎么写?
我自己写的话,我知道这是浅拷贝,我得识别这样的情况,像栈这样里面有资源的是不是啊?
我就不能让两个指针指向同一个空间
我拷贝你
是我的top和capacity跟你一样跟你一样,但我的_a不能和你一样,我是不是得有自己的空间啊?在这个地方不能照本宣科
深拷贝(再深一个层次去拷贝,不仅拷贝这个值,还要拷贝这个指针指向的资源)
所以默认的拷贝构造并不能满足所有的需求
我们再来回顾一个王炸型的问题大家理解一下:
C++为什么规定传值传参必须调用拷贝构造?
祖师爷为什么要做这件事情?
如果跟C语言一样,这个地方会发生什么?假设这是一个C语言的栈的结构体
1. 我把我的栈拷贝给你,那我们俩是不是就指向同一块资源了?里面改变是不是也会影响外面啊?
你以为我是传值,他是我的拷贝,他的改变不影响我,但它实际会影响你。
2. 它Destory,你再Destory,行不行?
不行
如果调用拷贝构造,我st的改变不会影响st1,是不是更合理啊?
这也从另一个角度警示我们-------自定义类型用传值传参还好不好?
不好
这儿传值传参,我本来里面啥事都没干,调用这个拷贝构造的代价很大,除了说直接赋制以外,还要开这个栈的空间(还要去堆上开空间)
假设我这个栈有10000个数据,那是不是更扯淡了
所以这些地方都不断的在告诫我们
函数传参尽可能用引用
如果不改变尽可能用const
所以你不要觉得C语言那个是更好的,祖师爷规定传值传参必须调用拷贝构造?是为了填C语言的坑,C语言在这些部分是很坑很坑的
像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。
MyQueue就像孩子一样
平时的吃穿用度你好像都没花钱,但你的爸妈花了钱
MyQueue有现在的生活,啥都不用做,那是靠栈
所以,真的要完成资源拷贝,总有人负重前行
这⾥还有⼀个⼩技巧
如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
一个类,如果你实现了析构函数的话,说明你有资源需要清理,那这时基本上就需要自己实现拷贝构造嗷
拷贝构造还可以这样写
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
之前引用部分的时候,我们说过,传引用返回我们讲一点点,剩下的是不是后面再讲啊?
传值返回会产⽣⼀个临时对象调⽤拷⻉构造
传引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。
但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引 ⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样
我们之前看到的都是传参的问题,现在我们来看一看传值返回的问题
我们调用Func2,传值返回这个地方会发生什么?
这儿是不是会产生一个对象的拷贝啊?
为了让这个地方的拷贝减少,可以用引用返回
传值返回和传引用返回的区别是
传值返回它不会返回st,它会返回st的拷贝,这个拷贝的临时对象不受Func2生命周 期的影响
有的时候会涉及到编译器优化的问题,在类和对象(下)再来讲
如果你不想让这个拷贝发生,加一个引用
加一个引用是不是就减少拷贝了?返回st的别名
但是这个时候会发生什么?
st是不是已经销毁了?
调用析构函数里面是不是全是野指针啊?
那这个时候是不是就非常的扯淡啊?
有没有完成拷贝啊?没有
为什么没有完成拷贝呢?
最核心的原因是因为st都已经销毁了
我们把这个样例从另一个角度再来看一看
编译器也会给你报一个警告
从当前这个程序来看,这样才是正确的
如果还有额外的参数的话,必须得有默认值
赋值重载
后面讲
后两个
取地址重载
-
普通对象
-
const对象
其实实际当中也不止6个,C++11以后又增加了两个,移动构造和移动赋值。
但这个阶段我们不讲这个东西。
有人会说:“我们不写,编译器不是会自动生成吗?那这东西有啥可学的啊?”
实际上不是这样的。
这几个函数为什么难学呢?
就是因为它们之间
1.知识点很多
2.它们之间又千丝万缕的关系,有很多要注意的点。
我们要从两个方面去学习
第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
它只有部分场景满足我们的需求,有部分场景不满足我们的需求
第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
那4个函数我们都要抱着这两个维度去学习。
1.要自己写的情况我们怎么写?它的规范是什么,要求是什么?怎么样才能满足合理的需求?怎么样写?
构造函数大多数情况都需要自己写,但是有些情况也可以让编译器自动生成。
下面我们来写一个栈
谁不需要自己写呢?
我们之前是不是讲过一个用两个栈实现队列的题目啊?
C语言的时候,我们是不是写了个MyQueue的构造函数啊?
如果用C++实现,是不是就是在MyQueue里面放两个栈啊?
我们还需不需要写MyQueue的构造函数啊?
不需要吧?
我们刚刚是不是讲对于⾃定义类型成员变量,要求调⽤这个成员变量的 默认构造函数初始化
C++里面只要是对象就会调用构造,调不到构造就会报错。
这两个栈是不是都初始化了?
所以,你也不能认为,编译器默认生成的构造啥用没有,少数场景下还 是有用的。
2.如果不写,编译器默认生成的需求是什么?
因为只有第一点,我才能知道我要不要写。
2.赋值运算符重载
2.1 运算符重载
这个东西我们之前提过,讲IO流的时候,插入流和提取流是不是提过啊?
当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错
内置类型都是支持各种运算符的,运算符包含什么呢?
加减乘除、比较大小等等等等
我们单说一个东西,比较大小,就拿我们的Date类来说
日期类能不能比较大小?----可以啊;而且比较日期大小,在现实中还是很需要的是不是?
举个简单的例子
有个程序,比如你实现了一个日期类
那我得比较一下,比如说这有一堆商品,我想把这些商品以日期由远及近排过来
那日期是不是就得比较大小啊 ?
那我们日期比较大小怎么实现呢?
我们内置类型的比较都是系统原生支持的
比如说我整型比较大小,浮点数比较大小
但是你自定义的类型支不支持比较大小啊?----不支持
自定义类型他要怎么去比较,它的行为应该是我们自己定义的,而不是由系统定义 的
我们普通的运算符都是针对内置类型
自定义类型它要怎么做?不知道
比如日期类你要怎么加?
所以你得自己去写一个运算符
所以,类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载。
若没有对应的运算符重载,则会编译报错。
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体
也就是说运算符重载它会转换成一个函数
重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数
那运算符重载的格式是什么呢?
规定是这样的
一个operator的关键字+运算符--->构成函数名
他也有
参数
那它的参数是什么呢?
参数,要看这个操作符是一元的还是二元的
咱们c/c++的运算符主要分为一元的和二元的
一元的就是一个参数
如:++
有的运算符,一个有两重含义
如
这个是不是可以表示乘啊
但是它也可以表示解引用
所以它可以重载成乘,也可以重载成解引用。这个我们后面迭代器的时候就会经常 玩到。
那它的参数是什么呢?看你需要什么。
二元就是两个参数
太多了,加减乘除,比较大小等等等等
比如说我们在这儿要比较日期类
我要传一个d1,一个d2
是不是就是两个参数啊?
返回值
它的返回值是什么呢?
看你这个运算符需要的是什么
比较大小,它的返回值是不是一个布尔值啊?
我们在这儿要支持,比较相不相等,怎么写?
但是我们在这儿面临着一个问题
我们是不是没有访问Date类的成员变量的权限啊(它们是private的,在类外不能直接访问)
不能访问这个私有怎么办?
有这样的几种解决方案
1.最挫的方案
先把这个取消,让它先跑起来
2.提供get函数(Java贼喜欢用)
你私有的我不能直接访问,但我是不是可以通过get间接访问啊?
注意,提供get和公有不一样啊!
如果你直接放成公有,别人在类外面是直接能修改的。
但是你提供get的话,不能修改。
get是获取到这个值嘛,这儿是传值返回,获取到的是它的拷贝。
上面那两种方式都不是太好,还有一种方式让我们解决这个问题
既然类外面不行,能不能放到类里面重载成成员函数呢?------可以啊
因为成员函数也可以写各种函数嘛
但是,当它重载成成员函数之后呢
要注意:它的第一个对象默认就传给了隐含的this指针。
所以它已经有一个参数了,那你是二元的
就变成明着写的是一个参数,暗着写的是一个参数
所以这个地方就变成这样写了
那我们要调用这个函数的时候怎么调用呢?
这样调
我们写的时候就这么写,他会自动转换从成上面的
如果你是一元的,就不写参数了
这块儿还有一个要注意的
比如说我在这儿定义了两个日期
传给二元运算符的时候,谁是第一个,谁是第二个呢?
首先,我们可以显式的调用这个函数
但是,这样调用跟写个普通函数有啥区别呢?
还可以怎么调用呢?
还可以这样调用
你这样写它也会转换成上面调用的这个函数
函数体
函数体就是你界定它的行为的时候
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致
不能通过连接语法中没有的符号来创建新的操作符(运算符):⽐如operator@
注意以下5个运算符不能重载。(选择题里面常考,大家要记⼀下
.*是一个c语言没有的运算符
举个样例带大家看一下
这里有一个类
这个类,假设我要去回调的方式去调用它的成员函数的指针
这个是普通函数的指针
普通的typedef都是这样的,前面是类型,后面是类型
但记住,函数指针,数组指针两个东西都比较特殊,无论是定义变量,还是 typedef类型,都要往里面放。
但是,它要声明这个指针类型的时候,成员函数还要加个这个
如果你想直接定义一个指针变量,就这样定义
如果不是成员函数就是这样定义的
当然我们用了前面的typedef之后就可以这样写了
所以一般函数指针最好都用一下typedef
假设我现在想实现一个回调
回调我要把谁给给它呢?
我要把func给给它
C++规定,普通函数的函数名就是函数指针
成员函数还得再加一个&
那我们要回调这个函数指针怎么回调呢?
以前是不是都是这样回调的?
现在,你这样回调不了
为什么回调不了?
普通的全局函数是可以这样回调的
但是成员函数有一个特殊的地方
它有隐含的this指针
你要调用这个函数指针,是不是得传隐含的this指针这个 实参啊?
那我能不能这样传一下呢?
不行
为什么?
this指针讲那一节的时候明确的讲过一个东西
我们的this指针在实参和形参的位置都不能显示
那怎么办?
C++规定回调成员函数的指针是这样回调的
这个时候就用到了一个运算符叫.*
也就是说, .*是在成员函数进行回调的时候用的
重载操作符(运算符重载)⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
内置类型直接用运算符就行了嘛,重载不是有毛病嘛。
⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意 义,但是重载operator+就没有意义
我们做个简单的比方
两个日期减有没有意义?有啊
一个日期减另外一个日期,它是天数
日期和日期相加呢?没有意义
没有意义就不会重载
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位 置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。
2.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象之间的拷贝赋值。
这里要注意和拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
这是拷贝构造。
它先是一种初始化的行为,再是拷贝。
它用于这样的场景:已经存在的对象之间的拷贝赋值。
特点
首先,他是一个运算符重载
并且规定它只能重载为成员函数,不能重载为全局的。
赋值重载的参数建议写成const类类型引用,否则传值传参会有拷贝。
只是建议,这个地方你就算写成传值他也不会无穷递归了。
为什么?
之前是拷贝构造去传值传参形成新的拷贝构造。
现在我是赋值重载,调用,那我传值传参,d2传给d,调用拷贝构造,拷贝构造调用完 是不是回来了?回来之后接着赋值啊,不会无穷递归
这就是一个最简单的赋值重载
2. 有返回值
且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。
为什么他是有返回值的呢?
因为赋值可能会存在这样的一些情况:
C语言是不是支持这样的赋值啊?
C语言是支持连续赋值的
这个赋值是这样走的:赋值运算符的结合性是从右往左走。
- 1赋值给k,赋值给k之后这个表达式是有一个返回值的,这个返回值是k,赋值表达式的 返回值是左操作数嗷。
- k又作为这个地方的操作数再赋值给j。这个表达式再有个返回值是j
- j再赋值给i
所以就实现了连续赋值
那我们重载自定义类型的运算符是不是也要实现这样的一个效果啊?
你这个地方d1要赋值给d3,它的返回值应该是d3
d1传给了d,d是d1的别名。
d3是传给谁的?d3是传给this的,this是d3的地址。
那怎么搞?
这么搞
this在形参里面不允许显式写,实参里不允许显式写,但是类里面允许你显式写this
但是这样写还是不好
传值返回也会生成一个拷贝,出了这个作用域这个this还在不在啊?在啊 (this指针的生命周期是和对象本身紧密相关的)。
那你在这儿用传值返回是不是就白白生成了一个拷贝啊?
所以这个地方用传引用返回。
这个时候我们回头来再看一个问题。
指针和引用虽然功能相似,但是指针能不能帮你替代调引用啊?
不能。
如果你非要替代它,那你就这样写了。
你这个就得改成这个,多挫啊。
所以引用在c++里面和指针是一对相辅相成的兄弟,各自有各自的价值。
3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数
我们这个地方不写它
这儿也能完成拷贝
因为Date类里面全是内置类型(Int)。
4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要
和拷贝构造函数一样。
2.3 日期类实现
日期+天数
日期加天数这里的逻辑是什么呢?
进位逻辑(和加法进位类似)
做个简单的比方
我在这个地方+10怎么加?
这太简单了
+50怎么加?
加法的原则是满了就进位。
这是不是满了?那是不是得进位啊?而且可能不止进一次。
这个地方真正的麻烦事在于每个月的天数不一样。
我们先把这个逻辑走完:7月是31天,所以这儿应该减31,往前进位。
基于这样的原因,我们在这儿写一个函数出来----GetMonthDay
把这个数组定义成静态的是因为它要频繁调用,那每次都创建这个数组就有 点浪费了(如果是),所以我直接放到静态区,是不是更好啊?
剩下就很简单了
//d1 += 100
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;
}
//d1 + 100
Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;
}
日期-天数
加是一个进位逻辑
那减是一个借位逻辑
这种很简单
但这个时候就要借位
如果你减出来是一个负数或0,那说明当前月的天数已经被减完了
比如这儿减出来应该是-37
那我要向6月去借,借出来要加到对应的这个天数上面,直到把它恢复成一个大于0的数。
//d1 -= 100
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){assert(_month > 0 && _month < 13);//先--月份,因为你要借的是上个月的天数--_month;if (_month == 0){_year--;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;
}
//d1 - 100
Date Date::operator-(int day) const
{Date tmp = *this;tmp -= day;return tmp;
}
我们可不可以-复用给-=啊?
可以
哪一种更好呢?-复用-=更好。它们之间真正的差异在-=上面
-的效率本身是更低的
这儿有两次拷贝
如果你用-复用-=呢?
也一样,两次拷贝无法规避。
所以,对于减自己而言,你用谁都是一样的
但是-=不一样
-=自己实现有没有拷贝?--没有
但是,-=去复用-就吃亏了,它拷贝了一次。
所以真正的差异点就在这个地方。
日期-日期
怎么减?
思路1
一步一步借,把它变成年月日都大的
天好说,年也还行,最麻烦的是月。
那我们换一种思路(思路2)
上面讲了两种方法都相对而言太麻烦了一些。
还有一种方法,我们可以尽可能的去复用我们之前的逻辑
如果我们从可能的去复用我们之前的逻辑的角度来出发的话:我们可以想象一下,是不是我们可以直接取小的那个日期啊?(我们不是有比较大小嘛)。取小了以后,让小的那个日期不断++。
中间经过了多少天,n就是多少。
那我们的基本逻辑就差不多了。
基本逻辑差不多了之后,现在可能还有一些小的问题。
不排除有人在这儿给你胡搞一个日期
所以我们在哪儿可以考虑一下把日期稍微检查一下呢?我们可以在构造函数的时候把日 期稍微检查一下。
如果我想让用户自己来输入日期呢?
scanf,printf能不能去输入输出自定义类型?不能
用成员函数间接输入也可以,但还是不方便。
所以这个地方有一个方法可以:我们可以用流提取和流插入重载我们的输入输出。
所以C++为什么要搞IO流这一套
普通的printf和scanf,它直接适应的是内置类型,它们是不是要指定这个百分 号符号啊?对于类类型它没办法很好的适应。
所以祖师爷设计流插入和流提取这一套是为了干嘛呢?
1.我支持内置类型
cout能直接使用内置类型的原因是什么呢?它库里面帮你重载好了
它能自动识别类型是源自于函数重载。
那我们现在想自己支持它的流插入流提取怎么办?我们是不是得自己重 载这个运算符啊?
我们说自定义类型要去用运算符得自己重载,重载了以后,它遇到这个 运算符他会自动转换成函数去调用。
那我们来写一下
先来写个输出(cout)(流插入)
运行一下
哪儿出错了?
参数不匹配
前面我们说了,对于二元运算符,它的左操作数对应第一个参数,右操作数 对应第二个参数。
这个地方能对的上吗?
对不上
所以这个地方真要调用得这么写
‘
这个写法可以倒是可以,但你有没有感觉这个写法就是倒反天罡 啊?
我本来想表达的意思是这个日期类对象流向IO流
但现在怎么变成控制台流向日期类里面去了?
这个时候怎么办?除了可以写成成员函数,运算符重载还可以写成全局 函数。
写成成员函数的目标是什么呢?为了访问私有成员变量嘛,方便嘛。
但是刚刚这儿写成成员函数的话,访问成员变量倒是方便了,但是 不对啊(关键是我调整不了这两个参数)。
所以我迫不得已就只能写成全局的。
我在这儿写成全局了,我就可以把cout传给第一个参数作我们的左操作数
但这个时候是不是访问不了私有啊?
那不就又回归到了我们之前的问题嘛?
成员函数的方法不能用了
那就提供get函数吧
那现在教大家一种新方法,这个新方法我们类和对象下才会讲,但是我 们现在可以说一下。
在类里面可以访问私有,在类外面不能。
但是有一种方式可以让类外面的函数访问类里面的东西:加一个友元声 明(友元函数)
我跟你是陌生人,我们俩不认识,是不是啊?
假如我和小明不认识。
那,我能不能去小明家里面玩啊?
不能是不是啊?但我特别想去啊?那怎么办呢?
所以,我要成为它的朋友
所以友元函数很简单,就是我要去你家里面玩。那我怎么办呢?我加一 个友元函数的声明。
这个我们类和对象下在详细讲,我在这儿只是浅浅的带大家用一下。因 为它很简单,就是增加一个关键字friend。
我是你的朋友(一般友元声明都喜欢加在比较前面的位置)
它不能实现成成员函数,它只能实现成全局的。
实现成全局的,它就涉及到访问私有的问题。
但是它并不是必须设计访问私有嗷,如果需要访问私有,那这个时候才 需要设计成友元。
流插入和流提取并不是说必须设计成友元嗷。
我们后面讲到string类那个地方就可以不设计成友元。
比如我不需要访问你的私有,我就不需要友元。
我需要访问你的私有,我才在这个地方设计成你的友元。
它呢还有一些没有解决的问题:如果我要连续的在这个地方输呢?
是不是不行啊?
原因是什么呢?
这个要看运算符的结合性。
我之前的赋值呢是从右往左结合,它呢是从左往右结合。
从左往右结合就是先走它
走它是不是转换成了一个函数调用啊?
cout传给out,d1传给d
调用完了,这个表达式是不是该有个返回值啊?
返回值一样还是左操作数,左操作数就是cout。
然后d2再流插入到cout。它是这样的逻辑走的嗷
out就是cout啊。所以我们返回out即可。
OK,没问题了。
这也支持了
再来写一下流提取(输入)
isream,ostream类型对象必须用引用嗷,不能传值
传值它会报错,因为它不只是拷贝构造
流提取这里就不能加const了
流提取在这个地方用cout会不会造成一些问题呢?
不会
之前说过cin和cout具有绑定关系
c++,第一它兼容C语言,所以它和C语言的缓冲区保持同步。也就意味着,比如你上面用的printf,下面用的cout,printf没有带刷新标志,因为它们都在缓冲区嘛,printf有缓冲区,cout有缓冲区。
我兼容C语言就是:我要进行刷新的时候,我会把前面的printf的缓冲区也进行刷新。
比如
printf没有进行刷新,cout进行了刷新,那cout刷新出去的时候,它会先把printf缓冲区的内容刷新出去。
所以它兼容C语言,两个不会乱。
但是这个东西会牺牲效率,所以在IO里面,如果要求效率比较高。
也就是说,我C++ cout刷新的时候,不去刷新C语言的printf。
这个是跟C语言的同步关掉
这个是跟其他的流全部绑定关系都关掉
其次,cin和cout也是绑在一起的
cin和cout为什么是绑在一起的呢?
就像我现在写的这个
cout是不是也是带缓冲区的?假设这一句到它的缓冲区里面之后
大家知道,刷新缓冲区是有一些要求的:比如遇到换行符,或者主动进行刷新,或者是程序结束等等等等这些才会进行刷新。
那这个进去了以后它不会到控制台,它只会在它的缓冲区。
所以当进行cin(in就是cin嘛)流提取的时候,它会去把cout主动刷新出去。
默认它和cout是绑在一起的。
默认情况下,cin是同步stdin(它跟C语言是同步的)。
其次,它默认是绑定到output,也就是cout的。
意思就是说cin刷新的时候会刷新cout的缓冲区
你在这儿走,这个东西到缓冲区了
我在这儿提取的时候。这个东西没有到缓冲区
因为它们各自有各自的缓冲区嘛,不用担心。
在你进行流提取之前,它会把cout的buffer刷新一下。
所以你在进行流提取之前这个一定出去了。
那我们现在就可以这么写
再来简单算一个
如果我输入一个这个日期呢?
咋回事?程序咋出不来了?
6月31是不是一个非法日期啊?
所以我们这个程序就应该再改进一下
这是不是就可以了?
//流输入
istream& operator>>(istream& in, Date& d)
{while (1){cout << "请依次输入年月日:>>";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "输入日期非法";d.Print();cout << "请重新输入!" << endl;}else{break;}}return in;
}
介绍一下运算符重载和函数重载
虽然他们俩都用了重载这个词,但是它们俩没有关系
函数重载指的是函数名相同,参数不同
运算符重载指的是重新定义这个运算符的行为
两个运算符重载的函数又可以构成函数重载,因为它们的函数名相同,参数不同。
实现一下比较运算符
两个日期比较大小怎么比呢?
那小于等于怎么写呢?
有没有必要cv呢?没有
教大家一种通用的方法,这种方法不仅仅对日期类有用,针对所有的类都有用。
写了一个<,下一个你就去写个==。
剩下的逻辑统统复用
>不就是<=取个反吗?
>=不就是<取反吗?
!=不就是==取反吗?
你必须得先写两个出来,先写>和=也可以。
再来实现一下++
这两个怎么区分啊?
二元是规定了左边是第一个参数,右边是第二个参数。
这俩都是一元运算符。这两个函数都不能同时存在。
所以祖师爷在这个地方是怎么考量的呢?
这样考量的。这两个人谁用的多呢?
是不是前置按理来说用的多啊?(前置的拷贝会少一些),前置是返回++以后的值,可以传引用返回。
后置++去做出改变,增加一个参数。这个参数你加不加形参名都行,编译器它不接收
编译器在这个地方也做了特殊处理。可以这么理解:
如果你是后置++,他会转换成去这样调用
这个地方的参数只是为了区分,所以可以不接收
实现
声明
定义
如果你是前置++,在这个地方就是正常的
实现
声明
定义
--
填个坑
有人会写这样的代码
贼坑
说明一个问题
你在实现+=(或-=)的时候,除了要考虑这个day是正数,是不是还要考虑这个day是负数啊?
-=也一样
Date.h
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>class Date
{//友元函数声明friend ostream& operator<<(ostream& out, Date& d);friend istream& operator>>(istream& in, Date& d);
public:Date(int year = 1990, int month = 1, int day = 1);void Print() const;// ?inlineint GetMonthDay(int year, int month){int MonthDayArry[] = { -1, 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;}return MonthDayArry[month];}//赋值Date& operator=(const Date& d);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 day) const;Date& operator+=(int day);Date operator-(int day) const;Date& operator-=(int day);//++d1Date& operator++();//d1++Date operator++(int);//--d1Date& operator--();//d1--Date operator--(int);//d1 - d2int operator-(const Date& d);//流插入//ostream& operator>>(const Date& d);int GetYear();int GetMonth();int GetDay();Date* operator&(){return nullptr;}const Date* operator&() const{return nullptr;}
private:int _year;int _month;int _day;
};//流输出
ostream& operator<<(ostream& out, Date& d);
//流输入
istream& operator>>(istream& in, Date& d);
Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
bool Date::CheckDate()
{if (_month < 1 || _month > 12|| _day > GetMonthDay(_year, _month)){return false;}else{return true;}
}
//构造函数
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!CheckDate()){cout << "非法日期" << endl;Print();}
}void Date::Print() const
{cout << _year << "/" << _month << "/" << _day << endl;
}
//赋值
Date& Date::operator=(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;return *this;
}//d1 < d2
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){return _day < d._day;}}else{return false;}
}//d1 == d2
bool Date::operator==(const Date& d) const
{return _year == d._year&& _month == d._month&& _day == d._day;
}
//d1 <= d2
bool Date::operator<=(const Date& d) const
{return *this < d || *this == d;
}
//d1 > d2
bool Date::operator>(const Date& d) const
{return !(*this <= d);
}
//d1 >= d2
bool Date::operator>=(const Date& d) const
{return !(*this < d);
}//d1 != d2
bool Date::operator!=(const Date& d) const
{return !(*this == d);
}
//d1 += 100
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;
}//d1 -= 100
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){assert(_month > 0 && _month < 13);//先--月份,因为你要借的是上个月的天数--_month;if (_month == 0){_year--;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;
}//d1 + 100
Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;
}//d1 - 100
Date Date::operator-(int day) const
{Date tmp = *this;tmp -= day;return tmp;
}//++d1
Date& Date::operator++()
{++_day;if (_day > GetMonthDay(_year, _month)){++_month;_day = 1;}if (_month == 13){++_year;_month = 1;}return *this;
}
//d1++
Date Date::operator++(int)
{Date tmp = *this;++*this;return tmp;
}
//--d1
Date& Date::operator--()
{--_day;if (_day <= 0){assert(_month > 0 && _month < 13);--_month;if (_month == 0){--_year;_month = 12;}_day += GetMonthDay(_year, _month);}return *this;
}
//d1--
Date Date::operator--(int)
{Date tmp = *this;--*this;return tmp;
}//d1 - d2
int Date::operator-(const Date& d)
{//找小的那个日期//假设法int flag = 1;Date min = d;Date max = *this;if (*this < d){min = *this;max = d;flag = -1;}int day = 0;while (min != max){++min;++day;}return day * flag;
}//int Date::GetYear()
//{
// return _year;
//}
//int Date::GetMonth()
//{
// return _month;
//}
//int Date::GetDay()
//{
// return _day;
//}//流输出
ostream& operator<<(ostream& out, Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}
//流输入
istream& operator>>(istream& in, Date& d)
{while (1){cout << "请依次输入年月日:>>";in >> d._year >> d._month >> d._day;if (!d.CheckDate()){cout << "输入日期非法";d.Print();cout << "请重新输入!" << endl;}else{break;}}return in;
}
3. 取地址运算符重载
那取地址运算符重载这个地方要看呢,这个地方就看一下什么东西呢?
3.1 const成员函数
首先得来看一下我们这个地方这里的成员函数。成员函数这里会提出一个const成员函数的概念。
const成员函数是说:用const去修饰成员函数
怎么修饰呢?放到成员函数参数列表的后面。
为什么要放到这儿呢?
我们的对象可能会有const对象。const对象去调用函数的时候是有一定程度的限制的。
比如用这个const对象去调用我们之前的print函数。
因为这里涉及到权限的问题,涉及到权限放大和缩小的问题。
还记不记得之前和大家讲过说,权限可以平移,可以缩小,但不能放大。
我们的对象有普通对象,也可能会有const对象。当const对象去调用普通成员函数的时候,会有导致权限放大。
那权限被放大了,你const对象不允许被修改啊。
这个地方是不是要取d1的地址传过去啊?d1地址的类型是什么?---const Date* 对不对? 因为它取了它的地址的话,这个const就是事实对象本身嘛,那这个指针就是指向内容不能修改。
那默认情况下,这个地方的指针是什么呢?
这个地方的指针我先讲简单一点啊,讲复杂了大家容易绕进去。
这个地方的指针是Date*
那大家看,这是不是一个经典的权限放大?
这样写的意思是这个年月日是可以修改的
我自己指向的内容都不能修改,你还能修改我自己的内容,那不扯淡呢嘛。
这个地方也有一个const。
但是这个地方的const是修饰this指针本身,不是指针指向的内容。
所以这个位置必须得想办法这样变才可以。
在前面想办法去加个const。
那这个地方怎么去改变this指针的类型呢?是不是没办法加啊?因为this指针我们当时讲的时候是这样讲的:this指针在类里面可以用,在函数里面可以用,但是在实参和形参的位置我们都不能写,我们不能碰它是不是啊?
所以祖师爷迫不得已在后面就出了一个偏方来治这个病。
这个偏方就是把const加在这个位置
现在还能不能修改?
不能
因为加了const以后,它本质上就变成这个了
严格来说是这个
第二个问题
如果这个对象是非const能不能调用?
可以
因为权限虽然不能放大,但是可以缩小
所以成员函数加上const有没有好处啊?有极大的好处,因为普通对象和const对象是不是都可以调用啊?
那我们能不能简单粗暴的把所有的函数都加上const呢?
不能行,因为有些函数(比如我们的构造函数)本身就要修改成员变量
构造函数你能加const吗?不能
你要让我初始化,你都把我加const,我咋初始化?
所以以后的原则是:不修改成员变量的建议把const加上。
3.2取地址运算符重载
取地址运算符重载之前为什么要先讲const成员函数呢?因为const成员函数本身也需要讲一讲。
放到这儿的原因是因为取地址运算符的重载有两个。
普通的取地址重载
const取地址重载
它里面其实就是返回this哦。
返回对象的地址嘛,this不就是对象的地址嘛。
那它为什么要重载两个呢?我重载一个const版本不就好了吗?
是这样的:一般它们俩都不需要重载,系统库直接就支持了
所以我们在这个地方写这个的时候是不需要重载的,其他运算符都需要重载
取地址运算符不需要重载
因为它是默认成员函数。实际当中,默认生成的就够用了,我们就不用管
除非我想使坏
假如我不想让别人取到我日期类的对象地址
这还不是最狠的,最狠的是我故意误导别人。我给别人一个随机地址。
那为什么他们俩两个都要写呢?
如果你是普通对象,你返回的是Date*
const对象,返回的是const Date*,如果你没写上面那个,普通对象也可以调用它,但是返回const Date*,就不合理。
所以有时候当一个函数的两个版本同时存在的时候是可以的。他俩构成函数重载(因为他俩返回类型不同),并且编译器去调用的时候会去调用最匹配的。