我们本期继续来学习C++的类与对象,没有看过往期的同学建议先看看之前的
(11条消息) C++类与对象—上_KLZUQ的博客-CSDN博客
目录
类的6个默认成员函数
构造函数
析构函数
拷贝构造函数
运算符重载
赋值运算符重载
日期类的实现
const成员
取地址及const取地址操作符重载
类的6个默认成员函数
我们在C语言阶段,多多少少都遇到过这样的问题
比如我们写了一个栈,经常在最后可能忘记释放空间,或者在开始时忘记初始化,然后一直报错,怎么都找不到问题,又比如我们在写一些oj题时,就会经常有内存泄漏的问题,这些问题,我们写起来又很繁琐,所以C++为了解决这些问题,就有了下面的内容
如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下 6 个默认成员函数。默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
注意,构造函数是创建函数,而是初始化,析构函数不是销毁对象,而是清理,名字可能让人误导
我们来看看这两个函数
构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(不需要写void)
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
我们来看例子
这里的构造函数就代替了Init的功能,Init就没有必要存在了(可以同时存在,不影响)
为了证明调用了构造函数,我们在构造函数里加一句代码
另外,我们可以从语法上把构造函数设置为私有的,但是设置了就不能调用了
析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
与构造函数相同,这里Destroy就没有存在的必要了,里面加了一句用来证明调用了析构函数
简单了解了构造函数和析构函数后,我们来看看他们的进阶内容
我们上面说了,构造函数是可以重载的,因为我们可能会有多种初始化的方式
比如我们创建栈时就有了数据
这段代码是没写构造函数的
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
为什么会生成随机值呢?这其实是当初C++设计的一个失误,如果用一些其他编译器,这里可能会初始化为0,但是C++标准并没用规定要初始化
C++将数据类型分为内置类型(基本类型)和自定义类型,内置类型是C语言自带的类型,比如int,char,指针等等,自定义类型就是我们用结构体,联合,class等等定义的
我们不写构造函数的话,编译器自己生成的构造函数会对内置类型不做处理,自定义类型会调用它的默认构造函数
比如我们定义了一个栈,我们发现栈中的元素被初始化了,这里内置类型也被初始化了,这是vs2019的处理,在13下并不会处理,正常情况下上面的_year等等都还会是随机值
这是13的情况下
所以我们一般情况下,有内置类型成员,就需要我们写构造函数,但如果成员全是自定义类型,就可以不写构造函数,让编译器去生成
因为这个失误,在C++11标准发布时,打了个补丁,在成员声明时可以使用缺省值
构造函数的定义很特殊,还可以重载,它的调用同样很特殊
调用是对象加参数链表,或者不加参数链表,这点需要记住
同时不能这样写 ,这是因为这样写可能会和函数声明冲突
这样也是错误的,如果这样写,为什么函数不叫Init呢?叫Init不是更好吗?所以不要想一些奇奇怪怪的东西
构造函数语法上是可以这样写的,我们一般也会写下面这种,全缺省或者半缺省,但是我们上面的代码就存在调用歧义了,如果我们用无参的就不知道调用谁了
我们再看一个知识点
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
什么是默认构造函数?总结一下就是不传参就可以调用的就是默认构造函数,这三个有且只能有一个,我们一般留一个全缺省的
析构函数也会默认自动生成,也是只处理自定义类型,而基本类型不做处理,但是析构函数不能重载
我们总结一下构造函数,1.一般情况下我们都需要自己写
2.如果内置类型成员都有缺省值,且初始化符合我们的要求我们可以不写,3.全是自定义类型构造,且这些类型都定义默认构造我们可以不写
再说一下析构函数,1.一般情况下,有动态申请资源,就需要显示写析构函数释放资源
2.没有动态申请资源,不需要写析构函数,3.需要释放资源的成员都是自定义类型,不需要写析构
比如我们的栈,它就是需要我们写析构的
日期类就不需要写析构
我们用两个栈实现队列,这种情况也不需要写析构
拷贝构造函数
我们在创建对象时,有可能会想要创建一个与当前对象一模一样的对象,这时就需要拷贝构造
拷贝构造函数 : 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型 对象创建新对象时由编译器自动调用 。拷贝构造函数也是特殊的成员函数,其 特征 如下:1. 拷贝构造函数 是构造函数的一个重载形式 。2. 拷贝构造函数的 参数只有一个 且 必须是类类型对象的引用 ,使用 传值方式编译器直接报错 ,因为会引发无穷递归调用。
我们创建了一个d1,然后用d1去创建d2,就需要拷贝构造,不过此时我们发现是有错误的
原因是拷贝构造有且只有一个参数,并且是类类型对象的引用,编译器会强制检查,否则会引发无穷递归
我们来理解一下是什么原因
拷贝构造,我们要先传参,但是实参传给形参,又形成了新的拷贝构造
这样讲很多人都理解不了,我们先按规定写好代码
按照规定,我们要用引用传参,我们加一条输出用来测试
我们先看这个,我们要调用func函数,我们要先传参
对于内置类型是没有规定的,比如这里我们传参10,就是直接拷贝这四个字节(int类型)
而Date是我们的自定义类型,规定自定义类型拷贝必须调用拷贝构造
我们在这里打个断点
我们f11后来到了拷贝构造 ,当走完拷贝构造后才会去func函数
传参就是一个拷贝构造,这是C++的规定
为什么这样规定,我们后续会讲解
如果这里拷贝构造使用传值会发生什么?
d2(d1)这里,在调用时要先传参,传参就会生成新的拷贝构造
这个新的拷贝构造相当于Date d(d1),用d1创建d,这里问题就来了,又要传参,调用拷贝构造,这里就成递归了
如果编译器这里不强制检查,这里就是无穷递归
这里用指针,引用都可以解开,但是用指针非常变扭,用引用就很舒服了
这里d就是d1,this就是d2
拷贝构造这里一般推荐加上const,防止不小心写错
这样不仅d2没被初始化,d1也没了
对于拷贝构造,若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
内置类型成员完成值拷贝(浅拷贝),自定义类型成员会调用它的拷贝构造,这里和构造和析构是不一样的
所以我们的Date这里就可以不写拷贝构造(值拷贝就是把这个对象按照一个字节一个字节的给拷贝过去)
这里我就把拷贝构造屏蔽了,我们可以看到是没有问题的
我们上面有一个栈,如果我们不写拷贝构造, 用栈来试一试呢?
这里直接就崩溃了
我们用调试来看这里其实是有拷贝的,但是这个拷贝我们是不想要的
仔细看一下,两个栈的数组是指向同一块空间的,所以这里最后析构空间时也就析构了两次,这就是崩溃的原因
另外,两次析构时,st2会先析构,因为栈帧也是符合后进先出
所以这种情况要我们自己实现拷贝构造,要实现深拷贝
比如我们这里st2要开一块和st1一样的空间,然后把st1里的数据拷贝到空间里,我们简单实现一下(以后会重点讲,我们现在涉及的知识还不到位)
此时我们就可以明白为什么要调用拷贝构造,而不是C语言一样直接拷贝过来,如果像C一样直接拷贝,就会一块空间析构两次,直接崩溃
就算不析构两次也会有其他问题,因为是指向同一块空间的,其中一个栈push的话,另一个也会受到影响
我们用两个栈来实现队列,这个队列也不需要写拷贝构造,因为全是自定义类型,会调用栈的拷贝构造(当然前提是栈的拷贝构造写好)
理解了这些内容,我们再来看下面的
运算符重载
假设我们有两个日期,我们想比较他们的大小该怎么办?
正常情况我们可能会去写一个函数来进行对比
有了这个函数后我们就可以去调用函数来进行比较
但是如果有人命名风格不好,给这个函数取名func之类的,然后注释什么也不写,那就很emmm了
如果像下面这样写就好了
直接用大于小于比较多好,但是日期类是我们自己定义的,C++是不知道我们要怎么进行比较
所以我们就有了下面的内容,C++把这个方式叫做运算符重载
使用一个operator的关键字即可,这里我们重载了小于
我们再将大于也进行重载
此时依然有错误,原因是 << 流插入的优先级很高(流提取也很高),所以我们要加上括号
这样就解决了,我们实现一下这个比较日期的函数
小于同理,然后我们测试一下(这里我们要先把类里的成员变量设为public,权限问题我们后续解决)
此时就可以进行比较了,非常舒服
为什么我们写了一个函数就解决了呢?因为编译器遇到内置类型它知道怎么比,转化为对应指令,而自定义类型会去查看是否重载operator,然后会自动转化为
就像this指针一样,这里我们是可以显示的去调用的,上下两行代码是等价的
我们上面说这个方法叫做运算符重载
对于日期类,哪些运算符是有意义的?
首先,我们的比较大小是有意义的,日期减日期也是有意义的,这个可以用来计算我们离某些时间还差多少天,比如我们今天离过年还有多少天
日期加日期是没有意义的,所以这里我们不需要重载+
是否重载运算符,取决于这个运算符对这个类是否有意义
但我们现在成员是公有的,我们来解决这个问题,我们之前也遇到过一样的问题,我们把函数写为成员函数就可以了
当我们写成成员函数后,发现会报错,这里说参数太多了
原因是C++对于运算符重载是有一些规定的
C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号 。函数原型: 返回值类型 operator 操作符 ( 参数列表 )注意:不能通过连接其他符号来创建新的操作符:比如 operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this.* :: sizeof ?: . 注意以上5 个运算符不能重载。这个经常在笔试选择题中出现。
这里要主义五个不能重载的,后面四个是域作用限定符,sizeof,三目运算符,一个点,而第一个是 .*(注意是一个点和*,而不是* )
这里就是因为参数多了一个,因为是有一个this的
我们这样写就好了,这样写其实是有两个参数,其中有一个是this
此时调用也要发生变化,变成d1.operator<(d2)
如果我们转到反汇编会发现指令其实是一样的,之前也是一样的
赋值运算符重载
我们之前说过有六个默认成员函数,我们下面要学习其中的一个,叫做赋值运算符重载
赋值运算符就是 = (等于号)
我们要这样写,就会调用一个函数
注意,d1=d2这里不是拷贝构造,这是已经存在的两个对象之间的复制拷贝
Date d3(d1) 这个才是拷贝构造,是用一个已经存在的对象初始化另一个对象
拷贝构造的本质是构造函数,赋值运算符重载本质是运算符重载函数
我们这样写是存在一些问题的
C语言是允许我们这样写代码的,这是连续赋值,是将0先赋值给 k,然后把这个表达式的返回值 k赋值给 j,然后再把这个表达式的返回值 j赋值给 i
那我们日期类是不是也可以这样写?
我们发现会出现这样的报错
原因是我们这里的返回值是void,为了解决这个问题,我们要这样写
如果我们是d4=d1,那么这里x就是d1,this就是d4的地址
我们要返回d4,所以要解引用,this不能在形参和实参显示加上,但可以在里面显示去用
此时我们再去编译就没有问题了,但这里其实还有一点问题
我们是传值返回,是不会返回this这个对象的,而是会返回这个对象的拷贝
比如这里返回的就是d4的拷贝
这样做是可以的,但是拷贝的代价太大了
如果我们运行就会发现调用了三次拷贝构造
我们之前说过,出了作用域还在,我们可以引用返回
此时运行就没有任何问题
那如果是这种情况呢?有些特殊情况就会用到这个操作
我们可以这样处理一下 ,我们用d1=d1举例,this是d1的地址,而x是d1,&x就是d1的地址
如果是d4=d1,this就是d4的地址,&x就是d1的地址,这里比较的是地址
还要注意这里不要写成 *this != &x,虽然写一下重载也是可以的,但是代价太大了
总结一下格式
赋值运算符重载格式参数类型 : const T& ,传递引用可以提高传参效率返回值类型 : T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回 *this :要复合连续赋值的含义
我们前面的成员函数不写编译器会默认生成,这里也是一样的
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝 。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
默认生成的赋值重载和拷贝构造的行为一样,对于内置类型成员完成值拷贝,对于自定义类型成员会去调用它的赋值重载
所以我们的Date和用两个栈实现的队列就可以不用我们去写赋值重载
而栈就需要我们去实现赋值重载,因为默认生成的是浅拷贝
日期类的实现
我们把定义和声明分开写
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool Date::operator<(const Date & d) {
if (d._year > _year) {
return true;
}
else if (d._year == _year && d._month > _month) {
return true;
}
else if (d._year == _year && d._month == _month
&& d._day > _day) {
return true;
}
return false;
}
bool Date::operator==(const Date& d) {
return d._year == _year
&& d._month == _month
&& d._day == _day;
}
我们先写出小于和等于的重载
我们还要实现大于,大于等于,小于等于的重载
这里可能很多人就会把上面的代码复制一下,然后修改修改,这样是可以的,但是我们有更舒服的办法
// d1 <= d2
bool Date::operator<=(const Date& d) {
return *this < d || *this == d;
}
我们可以复用之前的代码,小于不就是小于或者等于吗?我们就可以这样写,我们上面已经实现了小于和等于,所以这里就可以直接这样写
bool Date::operator>(const Date& d) {
return !(*this <= d);
}
bool Date::operator>=(const Date& d) {
return !(*this < d);
}
大于等于我们可以用大于或者等于来复用,不过我们上面写了小于,我们直接取反就可以
bool Date::operator!=(const Date& d) {
return !(*this == d);
}
我们还能重载不等于,不等于就是等于取反
所以以后我们重载大于小于这些关系时,都可以这样写,大于小于这些关系本来就是有互斥的
另外,运算符重载不一定是两个相同的运算符,比如可以日期加天数,我们前面说日期加日期没意义,但是加天数是有意义的
比如我们用4月26+2天,加20天,加100天,由于一年的每一个月天数都不同,并且2月还分闰年和平年,所以我们要写一个函数来帮助我们获取天数
int Date::GetMonthDay(int year, int month) {
static int dayArr[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 dayArr[month];
}
}
这里大家注意,我们if里面是先判断2月,然后判断闰年,这样效率更高一点(&&运算符的原因)
我们还把数组设置为static的,因为我们的函数会频繁调用,而每次调用都会创建数组,这样写就可以不用每次创建了
4月26加2天,没有超过4月的天数,不需要进位,4月26加20天,4月46天,超过4月的天数了,所以变成5月,并且天数减去4月天数,变为5月16
加100天同理,最后变成8月4日,如果月超过12就进年,并且月变为1月
Date Date::operator+(int day) {
_day += day;
while (_day > GetMonthDay(_year,_month)){
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
我们来测试看看
结果是我们想要的, 但是我们发现d1被改变了
同样的情况下,这里 i 是不会被改变的
所以其实我们实现的并不是+,而是+=
Date& Date::operator+=(int 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) {
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13) {
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
我们使用拷贝构造拷贝一个this即可,因为出了作用域tmp会销毁,所以不能用引用返回
我们还发现一个问题,这里d3(d2+100)写的好奇怪
用下面这种写法更舒服一点,那换成下面这种写法是有个 = 的,那这是赋值还是拷贝构造?
再比如这里的d4是赋值还是拷贝构造?
答案是拷贝构造,原因是我们上面说的,拷贝构造是用一个对象初始化另一个对象,赋值是已经存在的两个对象之间的复制拷贝
这两个写法是等价的(编译器会处理为一样的)
这种才是赋值,拷贝构造的特点是拷贝初始化,赋值的特点是纯粹的拷贝
所以上面的d3也是同理
我们上面的operator+和+=是否可以复用一下呢?我们来试一试
我们直接用+调用+=就可以,同理+=调用+也是可以的
那这两种复用哪一种好呢?答案是第一种好一点
原因是第一种+=不用创建对象,第二种里我们用+=调用+时,还要创建tmp
简单算一下,第一种创建2个对象,第二种创建4个
我们再来看重载++,++有前置++和后置++两种
我们实现他们很简单,但我们发现一个问题
他俩不可以同时存在,因为存在调用歧义,那该怎么解决呢?
我们要在后置++这里增加一个int参数,这里我们没写形参,因为接收了也没有意义,我们增加这个参数只是为了区分,为了占位
这里增加参数是C++的规定,设计者也是没有办法,只能这样设计,这里相当于牺牲了一下后置++
编译器就会这样转化一下
这里我们也发现,前置++效率是高一点的,因为后置++要创建一个对象
我们再来实现日期减天数,看看100天前是什么时候,1000天前是什么时候
减法是需要借位的,比如5月5日,50天前,5-50=-45,所以这时我们需要借4月的30天,变成4月,30-45=-15,我们还要借3月的31天,就变成了3月16日
Date& Date::operator-=(int day) {
_day -= day;
while (_day <= 0) {
--_month;
if (_month == 0) {
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
我们先实现-=,一会-直接复用-=即可,我们先让天数减去天数,然后不够再借前一个月的,月减少后要判断一下是不是变成了0月,如果变成0月,月变为12,年再减少即可,再让天数加上该月份的天数即可
我们再看-
Date Date::operator-(int day) {
Date tmp = *this;//拷贝构造
tmp -= day;
return tmp;
}
直接复用即可
大家再仔细想想,我们上面的函数都没问题吗?我们来看个奇怪的例子
很遗憾,我们对于负数还没有处理,所以我们要处理这个问题
我们这样写对吗?我们测试一下
还是错误的,这是因为这里的day任然是负数,即我们的代码变成了*this -= -100,所以我们还要加一个负号,把负100变为正100
这样就可以了(这里用abs也可以)
-=这里也同理
此时我们负数的问题也解决了
我们上面还实现了++,现在来实现--
Date& Date::operator--() {//前置--
*this -= 1;
return *this;
}
Date Date::operator--(int) {//后置--
Date tmp = *this;
*this -= 1;
return tmp;
}
和++一样,我们尽量使用前置比较好一点
我们下面再来实现两个日期之间相差多少天,比如今天与2000年1月1日相差多少天
日期相减并不简单,我们可以先用2023年1月1日减2000年1月1日,这样计算得到的就是整年,再加上闰年多的一天,再加上5月5到1月1的天数即可
如果是2023年的5月5减去2000年的12月1日,我们可以用2023年12月1日减去2000年12月1日得到整年,然后再减去12月1日到5月5的日期即可
这两种方法其实都有点麻烦,我们用下面这种办法,复用前面的代码即可
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 (max != min) {
++min;
++n;
}
return n * flag;
}
我们先使用max给日期大的,min给日期小的,然后max!=min,就++min,然后记录n即可,这里用!=比用大于或者小于效率要高,并且++min比min++要高,这些需要注意,flag是让天数变为正还是负的,比如过去减去现在,就是负数,现在减去之前,就是正数
我们上面打印都使用的是cout,而不是用print打印,这是因为cout可以自动识别,不过我们现在还不能用cout直接打印Date,而是用我们自己写的print函数,这时候我们就可以重载<<,流插入运算符,这样我们就可以用cout直接打印Date,<<是双目操作符
两个操作数一个是我们的日期类对象,另一个是cout,cout是类对象,是ostream的类对象
ostream是iostream库定义的,cin是istream的类对象
为什么可以自动识别呢?
其实根本没有什么自动识别,只是有函数重载罢了,而可以直接支持内置类型是因为库里面实现了
void Date::operator<<(ostream& out) {
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
当我们重载流插入后编译
又出现了这个错误
我们希望前面的语句可以转化为后面的语句,但很可惜这里转化不了
而这条语句可以转化
但是这样非常的变扭,第一个参数是左操作数,第二个参数是右操作数,这是规定的
比如这里的减法,加法可能不需要这样的规定,但是因为有减法,所以就有了这样的规定
这条语句是d1流向cout,cout是终端控制台,我们现在变成了控制台流向d1,我们想要改变的话只能去库里面改,可是我们不可能去改库,所以,流插入不能写成成员函数,因为Date对象默认占用第一个参数作为左操作数
void operator<<(ostream& out, const Date& d){
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
我们将这个函数变为全局后,还有一个问题,我们在全局是无法访问私有的
这里一个办法是写get方法
这种办法在java里就经常使用
C++里会使用一个叫友元函数的方法
使用friend关键字,在类里面进行函数声明,代表我是你的朋友,我可以用对象访问私有,就像现实里你邀请朋友去家里做客一样,朋友可以在你的家里吃饭,而陌生人不可以
我们还可以显示的去调用
我们上面Date前加了const,ostream里没有加,这里加上const就报错了
原因是流插入就是不断往cout里插入,在改变cout的状态,所以加上const是不行的
我们还发现,我们是不能连续打印的,但是我们使用cout时是可以的,而且这里连续的赋值是可以的
连续赋值是因为运算符的特性,是d3赋值给d2,d2作为返回值赋值给d1
而cout却不能从右往左执行,是应该先d1流向cout,再有一个返回值,这个返回值应该是cout,然后d2流向cout,d3再流向cout
ostream& operator<<(ostream& out, const Date& d){
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
再次修改代码,返回out即可
另外,友元只是一个声明,不受访问限定符限制,我们写在哪里都可以
就比如你邀请朋友在哪里都是一样的,无论现实里邀请还是电话里或者QQ等等,都是一样的
不过定义是在外边的,定义在内部就成成员函数了,就像朋友来家里做客不代表朋友就变成了家人
有了流插入,那就有流提取
我们来完成它
我们先看,因为是流提取,所以Date前是不能加const的,那istream前能不能加呢?我们先完成函数再看
istream& operator>>(istream& in, Date& d) {
in >> d._year >> d._month >> d._day;
return in;
}
这里告诉大家,istream前也不能加const,因为和cout一样,要改变in的状态
我们还要一些问题需要解决
我们在构造的时候,是没有限制的,可是月份是没有13月的
我们甚至可以构造成负的,为了解决这个问题,我们可以在构造时加个检查
Date::Date(int year, int month, int day)
{
if (month > 0 && month < 13
&& day > 0 && day <= GetMonthDay(year, month)) {
_year = year;
_month = month;
_day = day;
}
else {
cout << "非法日期" << endl;
}
}
如果更严厉一点可以在非法日期后加assert
这样就强制断死了
除了构造出的日期,我们输入的也可能存在问题
所以我们在输入时也检查一下
istream& operator>>(istream& in, Date& d) {
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day > 0 && day <= Date::GetMonthDay(year, month)) {
d._year = year;
d._month = month;
d._day = day;
}
else {
cout << "非法日期" << endl;
assert(false);
}
return in;
}
这里因为GetMonthDay需要对象才可以调用,但是我们现在先让代码先跑过去,后续再来解决,我们可以在类里面把GetMonthDay声明为静态的,在外部就可以向这样调用了
此时我们再来测试
问题就解决了
const成员
我们发现一个奇怪的现象,这里的d1可以调用print,而d2不能调用
这里的原因是,这里的函数调用会进行转化
print函数这里有一个this指针
在传参过程中,d1传递的是Date*,而d2传递是const Date*,这里const修饰,表示指针指向的内容不能修改
d1是权限的平移,而d2是权限的放大
如果想要传参,需要把this指针改为const Date*
this指针是隐藏的,我们不能修改,所以我们只能这样做
在函数后面加上const
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
我们在函数后加的const,修饰的是 *this
这样问题就解决了,我们发现加上const后,普通对象和const对象都可以调用
但也不能在所以函数后面都加const,比如要修改成员变量的函数就不可以加,比如我们重载的+=
但是+是可以加的,只要不改变成员变量都可以加
还有一个奇怪的问题
d1可以和d2比较大小,但d2不能和d1比较
这里也是相同的问题,d1的传参和d2的传参
所以这里能加const的我们都加上 (记得在定义也加上)
取地址及const取地址操作符重载
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main() {
Date d1;
Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
}
在vs19下const对象必须初始化,所以我们这里给了缺省值,这样就不用写初始化了,这里我们看到调用了这两个函数
这两个函数构成重载,不过这两个函数没什么价值,我们了解即可
如果我们不想让别人取到普通对象的地址就可以这样写
以上即为本期全部内容,希望大家可以有所收获
如有错误,还请指正