C++类与对象—中

news2024/9/22 11:25:39

我们本期继续来学习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++的规定,设计者也是没有办法,只能这样设计,这里相当于牺牲了一下后置++

C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传

 

编译器就会这样转化一下 

这里我们也发现,前置++效率是高一点的,因为后置++要创建一个对象

我们再来实现日期减天数,看看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对象必须初始化,所以我们这里给了缺省值,这样就不用写初始化了,这里我们看到调用了这两个函数

这两个函数构成重载,不过这两个函数没什么价值,我们了解即可

如果我们不想让别人取到普通对象的地址就可以这样写

 

以上即为本期全部内容,希望大家可以有所收获

如有错误,还请指正

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

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

相关文章

RabbitMQ启动失败的一系列尝试

一. 背景 在三台 CentOS 7.x上部署了RabbitMQ集群&#xff0c;如下&#xff1a; 服务器IPhostname节点说明端口管控台地址192.168.31.101masterrabbitmq master5672http://192.168.11.71:15672192.168.31.102slave1rabbitmq slave5672http://192.168.11.72:15672192.168.31.10…

centos7 安装 oceanbase 单机版测试

1&#xff1a; 说明 虚拟机&#xff1a;4核8G&#xff0c; 60G硬盘要求。 用的是社区版 官网地址&#xff1a; https://www.oceanbase.com/docs/common-oceanbase-database-cn-10000000001692850 下载地址&#xff1a; https://www.oceanbase.com/softwarecenter 2&#…

基于Linux系统在线安装RabbitMQ

一、前言 二、Erlang下载安装 三、RabbitMQ下载安装 三、RabbitMQ Web界面管理 一、前言 本次安装使用的操作系统是Linux centOS7。 二、Erlang下载安装 在确定了RabbitMQ版本号后&#xff0c;先下载安装Erlang环境。下面演示操作过程&#xff1a; Erlang下载链接&#…

JDBC基础

1、JDBC概述 在开发中使用的是java语言&#xff0c;那么势必要通过java语言操作数据库中的数据。这就是接下来要学习的JDBC。 1.1 JDBC概念 JDBC 就是使用Java语言操作关系型数据库的一套API 全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接 我们开发的同一…

【Linux】7. 环境变量

1. 环境变量的引入 先描述一个现象&#xff0c;我们在执行二进制可执行程序的时候&#xff0c;是需要找到其所在位置的(程序要运行必须先加载到内存&#xff0c;是因为冯诺依曼体系结构规定CPU只能从内存中读取数据)&#xff0c;所以这也就是为什么我们在运行前带上./的原因&a…

java http get post 和 发送json数据请求

浏览器请求效果 HttpUtils 工具类 package com.bysen.common.utils.http;import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ConnectException; impo…

一次业务系统无法使用对应的oracle数据库性能分析过程

一、问题概述 自助机系统及其它HIS等相关业务程序从3日早上8:20分左右出现使用异常&#xff0c;通过关闭自助机应用服务器及现场工程师KILL相关锁进程后正常。后续数据库工程师通过远程方式接入数据库环境进行问题排查&#xff0c;通过对相关日志等信息的深入分析&#xff0c;…

教育单位启动网络安全排查,查询IP地址能做什么?

近日&#xff0c;南京教育局主办了一场“网安2023”教育行动&#xff0c;各级教育单位都将开始教育系统网络安全责任制&#xff0c;做实各项教育网络安全工作&#xff0c;提升教育系统网络安全防护能力。 本次行动采取远程安全检查与现场检查相结合方式对各级教育单位进行网络…

Java爬取壁纸图片

Java爬取壁纸图片 前言依赖爬取图片工具类 -- WallHavenDownloadToDir.java 测试 前言 自己写了一个项目 想加一个功能&#xff1a;自动爬取壁纸发送给用户。说干就干&#xff0c;Python能干的&#xff0c;Java也能干&#xff01; 参考文章&#xff1a; jsoup的使用 Java爬虫…

STL-Vector容器

vector数据结构和数组非常相似&#xff0c;也称为单端数组 vector与普通数组区别&#xff1a; 数组是静态空间&#xff0c;而vector可以动态扩展 vector容器的迭代器是支持随机访问的迭代器 目录 创建vector容器 vector 容器赋值 vector容器的大小 vector插入和删除 ve…

java计算矩形的面积和周长的方法

在生活中&#xff0c;我们常常需要计算某个矩形的面积和周长&#xff0c;如我们经常用的计算器就是个不错的选择&#xff0c;它可以计算出任意一个矩形的面积和周长。那么&#xff0c;如果你想使用 Java编程语言来计算矩形的面积和周长&#xff0c;你该如何做呢&#xff1f;今天…

基于WiFi做呼吸频率检测-python版

一、概述 本Demo无需机器学习模型&#xff0c;Demo功能涉及的理论主要参考了硕士学位论文《基于WiFi的人体行为感知技术研究》&#xff0c;作者是南京邮电大学的朱XX&#xff0c;本人用python复现了论文中呼吸频率检测的功能。Demo实现呼吸速率检测的主要过程为&#xff1a; …

C# 对PdfiumViewer工具栏进行自定义,实现放大缩小,首页, 尾页,上一页等功能。

文章目录 前言PdfiumViewer工具栏扩展1 创建winform工程&#xff0c;UI界面2 打印预览3 放大功能4 缩小功能5 按比例缩放6 全屏7 首页和尾页8 上一页和下一页9 页码输入框10 显示当前预览的页码 小结 前言 关于PdfiumViewer的介绍 C# 使用PdfiumViewer实现对PDF文档打印预览&a…

【论文学习】ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation

ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation 目录 ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation摘要1 介绍2 DNN说话人识别系统2.1 Extended-TDNN x-vector2.2 基于ResNet的r-vector 3 提出的ECAPA-TDNN架构3.1 依赖于…

【Unity之c#专题篇】—核心章题单实践

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

电脑开关机-第14届蓝桥杯省赛Scratch初级组真题第1题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第130讲。 电脑开关机&#xff0c;本题是2023年5月7日举行的第14届蓝桥杯省赛Scratch图形化编程初级组真题第1题&#…

flstudio21有什么新功能,主题随心换,苹果M2/1家族芯片原生支持

FL Studio 21推出 – 新功能和改进。如果您从事音乐制作&#xff0c;那么您不可能没有听说过 FL Studio&#xff0c;或者很可能已经使用过这个音乐程序。好了&#xff0c;新版本的 FL Studio 21 DAW已经准备好向公众发布了。Image-line 正在为 2023 年的大型揭幕准备最终细节。…

go 源码解读 - sync.WaitGroup

go version 1.19.7 在 Go 语言中&#xff0c;sync.WaitGroup 是一个并发编程的同步工具&#xff0c;用于等待一组 Goroutine 执行完毕。 当需要等待多个 Goroutine 完成任务后才能执行下一步操作时&#xff0c;我们可以使用 sync.WaitGroup 实现协程间的同步。它提供了 Add()…

测试:概念篇

目录 简单介绍测试 我们先简单的介绍一下测试工程师 简单来看看测试和开发的区别 测试的基本概念 什么是需求 BUG 的概念 测试用例 什么是测试用例&#xff1f; 为什么有测试用例 测试周期 开发模型 瀑布模型&#xff1a; 螺旋模型&#xff1a; 敏捷软件开发 V …

PostgreSQL 查找重复数据(二)

创建表和测试数据&#xff1a; -- DROP TABLE IF EXISTS people; CREATE TABLE people (id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,name varchar(50) NOT NULL,email varchar(100) NOT NULL );INSERT INTO people(name, email) VALUES (张三, zhangsantest.com),(李…