【C++】类和对象【中下】

目录

个人主页<---请点击
C++专栏<---请点击

一、类与对象

本期的主题是一步步完善日期类的编写,将要讲解的知识融入在代码中。

1、运算符重载

  • 当运算符被用于类类型的对象 时,C++语言允许我们通过运算符重载的形式指定运算符新的含义C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载若没有对应的运算符重载,则会编译报错。
  • 运算符重载是具有特殊名字的函数,它的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型参数列表 以及函数体。
  • 重载运算符函数的参数个数 和该运算符作用的运算对象数量 ⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数。
  • 如果⼀个重载运算符函数是成员函数 ,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个 。(this指针在上期博客中有讲解)
  • 运算符重载以后,其优先级结合性 与对应的内置类型运算符保持⼀致。
  • 不能通过连接语法中没有的符号来创建新的操作符:如operator@
  • 下面5个运算符不能重载.*、::、sizeof、? : 、.
  • 重载操作符至少有⼀个类类型参数 ,不能通过运算符重载改变内置类型对象 的含义,如: int operator+(int x, int y)

我们依旧以实现过的日期类 作为例子,这里我将源代码分为3部分管理,分别是test.cpp、Date.h、Date.cpp
初始代码
Date.h:

c 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	void Print();
	
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_day = d._day;
		_month = d._month;
	}

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

Date.cpp:

c 复制代码
#include "Date.h"

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

test.cpp

c 复制代码
#include "Date.h"

int main()
{
	Date d1(2025, 1, 1);

	return 0;
}

⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义 。我们知道日期加上天数是有意义的,能够让我们知道过了若干天后是几年几月几日。接下来我们来实现一下+=的运算符重载operator+=

在实现重载运算符之前我们还要考虑如果天数加多了,要考虑进位的问题,所以我们先把每年的几月有多少天实现一下 ,便于我们实现+=运算符的重载。
GetMonthDay:

这个函数定义为日期类的成员函数。

c 复制代码
int GetMonthDay(int year,int month)
{
	assert(month > 0 && month < 13);
	int a[15] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && ((year % 4 == 0 && year % 400 != 0) || (year % 400 == 0)))
	{
		return 29;
	}
	else return a[month];
}

判断闰年,然后对2月特殊处理,返回那一月的天数即可。

operator+=

c 复制代码
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}

由于成员函数的声明和定义分离,所以访问时要加上Date::,我们已经实现了日期加上天数,下面让我们验证一下正确性。

c 复制代码
int main()
{
	Date d1(2025, 1, 1);
	d1.Print();
	d1 += 300;
	d1.Print();

	return 0;
}

这里+=面对类对象 ,编译器会自动处理为调用运算符重载函数d1传给了第一个参数,也就是隐含的this指针,300传给了day

运行测试:

那我们如果不想让d1中的天数发生改变该怎么办呢?这时候我们就可以实现operator+了。

c 复制代码
Date Date::operator+(int day)
{
	//调用拷贝构造创建另一个类
	Date tmp(*this);

	tmp += day;

	return tmp;
}

我们已经实现了+=,所以我们可以利用+=实现+这里返回的时候就不能使用引用了,因为调用函数结束后tmp会销毁,这里返回的时候程序会创建一个临时对象 同时调用拷贝构造函数为临时对象初始化,然后临时对象传出去。

c 复制代码
int main()
{
	Date d1(2025, 1, 1);
	//拷贝构造
	//Date d2(d1 + 10000);
	//等效Date d2 = d1 + 10000;
	Date d2 = d1 + 10000;
	d1.Print();
	d2.Print();

	return 0;
}

运行测试:

注意这里还有一处细节,就是临时对象是具有常性的 ,不能被更改,我们知道执行Date d2 = d1 + 10000;的时候会首先调用operator+函数将d1传给this指针,10000传给day,等到返回的时候,要建立临时对象 ,假设叫做zmp会隐含执行 const Date zmp(tmp);由于临时对象具有常性 ,所以d2的那段代码会处理为Date d2=zmp;此时又会调用我们写的拷贝构造函数 ,如果我们的拷贝函数是Date(Date& d)时,就会报错,因为涉及到了权限的放大zmp相当于const Date肯定不能转换成Date

注意 :这里的自己写的拷贝构造函数的参数必须是 const Date& d,当然这里编译器自己生成的拷贝构造函数对于日期类来讲也可以满足需求,你也可以不写。

1.2 赋值运算符重载

赋值运算符重载 是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值 ,这里要注意跟拷贝构造区分拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。

c 复制代码
//拷贝构造
Date d1(2025,1,1);
Date d2 = d1;

//赋值运算符重载的场景
Date d1(2025,2,2);
Date d2(2025,1,3);
d2 = d1;

赋值运算符重载的特点

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数 。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝,浪费资源。
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
  • 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用它的赋值重载函数。小技巧 :如果⼀个类显示实现
    了析构并释放资源,那么它就需要显示写赋值运算符重载,否则就不需要。

我们现在可以去尝试写一下operator=:

c 复制代码
void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
c 复制代码
Date d1(2012, 3, 9);
Date d2(2025, 1, 1);
d1 = d2;
d1.Print();
d2.Print();

测试结果:

我们这里的确写出了operator=,并且也起作用了,但我们没有遵从规则中的:有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景,这里的场景是为了满足连续赋值;假设我们现在执行下面这段代码它会报错的:

因为 我们的赋值重载函数没有返回值 ,此时将void改为Date&就可以解决问题。

c 复制代码
Date& operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;

	return *this;
}

1.3 <<运算符和>>运算符

还记得我们写打印的时候用到的<<吗? 它是流插入运算符 ,本质上也是运算符重载

<<重载了各种各样的内置类型,所以它用起来才会这么好用。

这时候对于自定义类型,我们可以自己重载<<运算符,使其能够以合适的方式输出自定义类型的对象,方便进行输入输出操作。
operator<<:

这里为了方便连续输出<<返回值 要用ostream&

c 复制代码
ostream& operator<<(ostream& out)
{
	out << _year << "/" << _month << "/" << _day << endl;

	return out;
}

注意:此时函数是类中的成员函数。

c 复制代码
Date d3(2025, 1, 3);
cout << d3;

测试结果

居然报错了?!我们再来仔细看看函数,发现第一个参数是this指针,而第二个参数是输出流对象,原来是我们传反参数了,我们翻一下:

c 复制代码
Date d3(2025, 1, 3);
d3 << cout;

虽然这次我们的代码可以正常运行,但还是感觉哪里怪怪的,欸,这不倒反天罡了吗 ?这更不行了,我们知道C++的类中的成员函数的第一个参数是this指针,我们不能在函数的实参和形参中显示的写this指针,所以只要写在C++类域中,第一个参数就只能是this指针唯一的方法就是定义在类外

  • 重载<<>>时,需要重载为全局函数 ,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第二个形参位置当类类型对象
c 复制代码
ostream& operator<<(ostream& out, const Date& d)
{
	cout << d._year << "/" << d._month << "/" << d._day << endl;

	return out;
}

这里我们将它定义成了全局变量,但是当面临全局变量的时候,又遇到了新的危机我们定义成全局变量没办法访问类域中的成员呀,它们可是私有 ,此时我们可以把这个定义成友元函数 (简单使用,后期博客还会讲解),就是在类域中声明这个函数是类的好朋友,可以允许这个函数访问私有成员。

此时程序运行起来就没有问题了:

>>:
>>流提取运算符 ,本质上也是运算符重载

>>中也重载了各种各样的内置类型,所以它用起来才会这么好用。

这时候对于自定义类型,我们可以自己重载>>运算符,使其能够以合适的方式输出自定义类型的对象,方便进行输入输出操作。
operator>>:

这里为了方便连续输出<<返回值 要用istream&,借鉴operator<<写成类成员函数的情况我们知道this指针依旧会占第一个参数的位置,所以我们要定义成全局变量。然后为了访问类中的成员变量,我们依旧要借助friend友元函数。

operator>>:

c 复制代码
istream& operator>>(istream& in, Date& d)
{
	cin >> d._year >> d._month >> d._day;

	return in;
}

注意这里要向d对象中输入年月日的值,所以形参不能写成const Date& d,这样就不能向其中输入数据了,就会报错。

Date类中的声明:

c 复制代码
friend istream& operator>>(istream& in, Date& d);

运行的代码片段

c 复制代码
Date d3(2025, 1, 3);
cin >> d3;
cout << d3;

运行结果


代码的运行是没有问题的,这样就完成了自定义类型输入的重载函数。

1.4 前置++与后置++

前置 ++后置 ++它们的函数重载名称都是operator++,无法很好的区分它俩,于是C++规定,后置 ++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。

c 复制代码
//++d1;编译器处理-》d1.operator++();
Date& operator++();
//d1++;编译器处理-》d1.operator++(n);
//n是随便的一个整数值
Date operator++(int);

前置++

c 复制代码
//++d1
Date& Date::operator++()
{
	*this += 1;

	return *this;
}

++d1d1的本身会改变所以可以返回它本身,而d1++d1不会改变,所以这时候就要再拷贝构造创建一个类对象,然后返回它。我们看到就是+=1,因为这里+=符号已经重载过了,所以这里会调用operator+=函数来实现+=

c 复制代码
//d1++
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;

	return tmp;
}

测试代码片段:

c 复制代码
Date d1(2025, 5, 1);
Date d2 = d1++;
d1.Print();
d2.Print();
Date d3(2025, 5, 1);
Date d4 = ++d3;
d3.Print();
d4.Print();

这里大家可以看到我们的operator=也派上用场了。

运行结果:

2、 const成员函数

  • const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
  • const实际修饰该成员函数隐含的 this指针,表明在该成员函数中不能对类的任何成员进行修改。例如:const修饰Date类的Print成员函数,即void Print() const;Print隐含的this指针由Date* const this变为const Date* const this

如果我们没有添加const,然后我们执行下面这段代码就会报错。

c 复制代码
const Date d1(2025, 5, 1);
d1.Print();


原因

但是我们又不能在成员函数的形参上显示定义this指针,所以此时C++才做出开头展示的规定。

c 复制代码
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

这时候就是const成员函数,只要不改变调用对象本身的成员函数都建议加const

代码运行

3、取地址运算符重载

取地址运算符重载 分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数,编译器自动生成的就用了,不需要去显示实现。

运行以下代码

c 复制代码
Date d1(2025, 5, 1);
const Date d2(2025, 5, 2);
cout << &d1 << endl;
cout << &d2 << endl;

当没有实现这两个函数时:

所以一般这两个函数我们没必要去实现,编译器自动生成的就满足需求了。除非是你不希望别人获取到这个类对象的地址,此时就可以自己实现。

c 复制代码
Date* operator&()
{
	return nullptr;
}

const Date* operator&() const
{
	return nullptr;
}

有两份函数,它们构成函数重载,此时编译器会针对不同的类型,普通类对象调用上面,const修饰的调用下面。

运行结果

这样就得不到类对象的地址了,当然这还不是最坏的,甚至可以给你返回一个假地址

c 复制代码
Date* operator&()
{
	return (Date*)0x00FF1120;
}

const Date* operator&() const
{
	return (Date*)0x00FF4330;
}

运行结果

地址看着也没有问题,这时候能把你坑死。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
俺不是西瓜太郎´•ﻌ•`几秒前
欧拉降幂(JAVA)蓝桥杯乘积幂次
java·开发语言·蓝桥杯
wuqingshun314159几秒前
蓝桥杯 10. 安全序列
c++·算法·职场和发展·蓝桥杯·深度优先
2345VOR2 分钟前
【Gurobi安装和申请教程附C#案例】
开发语言·c#·求解器·gurobi
梁下轻语的秋缘3 分钟前
每日c/c++题 备战蓝桥杯(洛谷P3382 三分法求极值详解)
c语言·c++·蓝桥杯
橙子199110165 分钟前
Kotlin 中该如何安全地处理可空类型?
开发语言·kotlin·log4j
ST_小罗10 分钟前
【Web前端】JavaScript入门与基础(二)
开发语言·前端·javascript
Livan.Tang44 分钟前
C++ 设计模式
开发语言·c++·设计模式
woho7788991 小时前
伊吖学C笔记(3、字符、分支结构)
c语言·开发语言·笔记
小L~~~1 小时前
C++高频面试考点 -- 智能指针
c++·面试
十一29281 小时前
C++标准库中 std::string 类提供的 insert 成员函数的不同重载版本
开发语言·c++