目录
简介
日期类总代码 | Date
类的定义 & 构造 & Print
类的定义
构造函数 & Print
比较类,如<、>、<=......
值加减类,如+、-、+=、-=......
加减类具体分类
判断某个月有多少天 GetMonthDay
日期类 + / +=(- / -=) 整数
日期类 - 日期类
日期类++ / --(前置后置)
流相关,如<<、>>(cout、cin)
结语
简介
日期类Date实现的本质就是 运算符重载 和 赋值运算符重载
我们要实现自定义类型像内置类型一样相加减,肯定会用到operator关键字+操作符
接下来我们要实现的分为三类:
- 比较类,如<、>、<=......
- 值加减类,如+、-、+=、-=......
- 流相关,如<<、>>(cout、cin)
日期类总代码 | Date
如果有需要代码的同学,下方的链接是我上传到 gitee 里的代码
2024 - 5 - 3 - gitee - Date
类的定义 & 构造 & Print
我们会建三个文件:
- test.cpp
- Date.cpp
- Date.h
其中我们的Date.h是头文件,我们的函数声明与类的定义,以及头文件都被包含在头文件里面
另外,我们在正式开始开始讲解日期类之前,我们需要先将类给定义出来,然后将其的构造函数、析构函数、Print函数写好
类的定义
代码如下:
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
构造函数 & Print
我们现在写函数都需要现在类中声明一份,然后再到Date.cpp中去实现逻辑,最后到test.cpp中进行测试
Date.h 内部代码如下:
class Date
{
public:
//构造函数声明
Date(int year = 1, int month = 1, int day = 1);
void Print();
private:
int _year;
int _month;
int _day;
};
Date.cpp 内部逻辑实现:
Date::Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
void Date::Print()
{
cout << _year << " "
<< _month << " "
<< _day << endl;
}
由于我们分成了头文件与.cpp文件,所以我们在.cpp文件中实现逻辑之前我们得先Date::一下
不然我们就没法使用我们定义的类
比较类,如<、>、<=......
在开始之前,我得先介绍一个符号防止有同学没听说过:!
这个是逻辑取反:0 取反就是非 0,非 0 取反就是 0
对于这一类,我们的实现逻辑是这样的:
我们先实现<,然后再实现==
当我们将这两个实现了之后,我们就可以复用这两个,然后将其余所有比较类的函数全都表示出来
试想一下:我们实现了<和==,当我们要实现<=怎么办
那是不是同时满足<和==就算是<=
这是我们要实现>怎么办
当一个数不<=时,即<=取反( ! 符号)就是 >
闲话少叙,我们先来实现一下 小于<
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;
}
注意看,我们在上文中实现的逻辑是:如果年小于,那就小于
如果年相等(大于的情况统一出判断之后返回false),那我们就看月,如果月小于,那就小于
如果月相等,那就看天,天小于,那就小于
如果上述条件都不符合,那就是大于或者等于,反正就是不小于
注:我们这里要的只是小于,如果等于或大于,那就不是小于
接着我们来实现一下 等于==
bool Date::operator==(const Date& d)const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
等于的实现逻辑相当简单,只有在我的年月日都相等的时候我的两个日期才想等
但凡有一个不相等,那就是不相等,全相等我们就直接返回一个 1,反之则是 0
接着就是很有意思的复用环节了,我们来实现一下 小于等于<=
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 !(*this == d);
}
我们能看到,大于就是小于等于取反
大于等于就是小于取反
不等于就是等于取反
值加减类,如+、-、+=、-=......
加减类具体分类
加减类里面其实还分了三类:
- 日期类 + / +=(- / -=) 整数(某个日期的多少天之后)
- 日期类 - 日期类(宝宝,今天离我们刚开始在一起已经过了多少天了呀)
- 日期类++ / --(前置后置)
判断某个月有多少天 GetMonthDay
在正式开始讲之前,我们为了防止后序的逻辑太乱,所以我们就单独将判断某个月有多少天的逻辑拿出来单独实现
而这个函数我们可以在类里面实现,这是为了类的包装,增加安全性、
当然如果你非要将其定义在全局也不是不可以,随你喜欢,出事了与我无瓜
接下来我们来讲一讲判断某个月有多少天的逻辑
由于一年有12个月,如果一个月一个月地返回的话,那就太挫了
所以我们可以创建一个数组,返回之时我们就可以把月当下标返回了
int MonthDayArray[13] =
{ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
可以看到,我们创建了一个13个数的数组,因为第一个的下表为0,我们要的效果是1月对应下标1
但是平年和闰年的判断关乎了二月的大小,所以我们不妨假设二月是28天
接着我们在下面判断今年是不是闰年且 要返回的月份是不是2月,如果不是二月,那我们判不判断闰年其实没意义
所以,如果月为2月,如果传过来的年判断为闰年,我们直接返回29即可
如果不是闰年或者根本就不是二月,那我们就返回每个月对应的对应数组下标中的值即可
代码如下:
int GetMonthDay(int year, int month)
{
assert(month < 13 && month > 0);
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 % 400 == 0))
return 29;
else
return MonthDayArray[month];
}
日期类 + / +=(- / -=) 整数
首先我们先来想一个问题:+和+=之间有什么不同
+是将值传过去,自身并不会改变,比如 int i = n + m; 其中 n 和 m 并没有改变
而 += 是自身的值会改变
所以我们面对这两类函数时,返回值也不一样,因为面对自身不改变的情况,我们只能创建一个临时变量,但是临时变量出了函数会销毁,所以我们的返回值不能为引用,需要让其调用拷贝构造函数,所以返回值类型为 Date
但是如果是对象自身要改变的情况,那我们就无需创建临时变量,直接在原对象的基础上进行改变即可,最后直接返回*this,所以返回值的类型需为引用,为了防止调用不必要的拷贝构造影响效率,所以参数的返回值类型为 Date&
我们先来实现一下 +=
+= 的逻辑如下:
我们先将 _day 与 day 直接相加,然后我们再对 _day 进行减小,每次减一个月的天数
我们while循环内的逻辑就是,当我的 _day 减小到比 GetMonthDay 函数返回的天数要大时,就证明我们的天数还需要再减
就好比我的月本来是 24,我想知道 100 天后是那一天,我就先 24 + 100 = 124
但是124肯定不止一个月的量,所以我们需要减
假设现在是3月,我就124 - 31 = 93,然后就来到了 4 月,但是还是有多,我就继续减,现在是 4 月就减 30,93 - 30 = 63......
直到 _day 是正常的,循环终止
我们每减一次日期,我们的月就要++一次
当我们的月为13时,我们就将13手动置为1,随后年++
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;
}
接着我们来实现一下 +
+其实就可以直接复用+=
我们可以直接创建一个和*this一摸一样的Date类型对象tmp
我们只需要对tmp进行改变(+=)最后返回即可
代码如下:
Date Date::operator+(int day)const
{
Date tmp = *this;
tmp += day;
return tmp;
}
接着是 -= 的实现
-= 的逻辑和 += 很类似,只不过我们现在是直接 _day - day
如此一来,我们的 _day 就为负数,我们就需要不断对其进行加的操作,并在此过程中注意月的--和年的--
但是有一个点需要注意的是,如果调用这个函数的人,要-=却传了一个负数过来,那就单独对其进行判断
如果传过来的是负数,那我们就将其 += 上负的参数即可
代码如下:
Date& Date::operator-=(int day)
{
if (day < 0)
return *this += - day;
_day -= day;
while (_day <= 0)
{
_day += GetMonthDay(_year, _month);
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
}
return *this;
}
最后是 - 的实现
类似的,我们的减就是实例化一个和 *this 一摸一样的临时对象 tmp(拷贝构造),然后对其进行 -= 操作,最后返回 tmp 即可
代码如下:
Date Date::operator-(int day)const
{
Date tmp = *this;
tmp -= day;
return tmp;
}
日期类 - 日期类
可能有人会疑惑,为什么只有日期类的-,而没有+
这是因为日期类的+是没有意义的,试想,2024年1月13号 + 2024年3月31号有什么意义?
而 - 之所以有意义是因为,如果未来有一天你的对象问你:宝,这是我们在一起的第几天啊?
也就是两个日期之间相差几天,这个是有意义的
而我们要实现这个的逻辑也较为简单:
我们只需要将两个日期分为大和小
我们找出小的那一个,不断让其+=1,然后再额外设置一个整形变量,小的那个日期类每+=1一次,整型变量就 ++ 1次
当小的那个日期和大的那个日期相等的时候,我们创建出来的那个整型变量的值就是两个日期之间的差值
当然,为了防止两个日期之间的差值为负数的情况,我们应该先将this指针指向的设置为max,另一个参数指向的为min,如果此时发现设置的max比min要小,那我们就将两者进行交换,随后设置一个变量flag,如果存在max小于min而后两者需要交换的情况,那我们就将flag赋值为-1,否则flag为1
最后返回的结果是两个日期之间的差值*flag
代码如下:
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
int count = 0;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
while (max != min)
{
min += 1;
count++;
}
return count * flag;
}
日期类++ / --(前置后置)
其实我们++或--的逻辑在我们将+=和-=实现完了之后都变得非常简单
我们++本质将就是+=1,那我们之间将日期类+=1即可实现++
直接将日期类-=1即可得到--
需要详细讲解的是前置与后置
我们前置与后置唯一的区别就在于,一个是先++后使用,一个是先使用后++
但是我们在写操作符名称的时候,都只能写operator++,所以我们无法分辨哪个是前置哪个是后置
本贾尼博士也发现了这个漏洞,所以就规定说,在传参数的括号中加上一个int的就是后置++
而这个int并不是说要传值的意思,里面传多少都没用,可以理解为就是对编译器的一个提醒,告诉编译器我现在的这个++是后置++
而我们前置与后置的函数返回值类型也是不一样的
前置是直接对*this指向的内容进行修改,修改完后直接返回*this即可,而*this并不是临时变量,出了函数并不会被销毁,所以我们函数的返回类型是引用,这样可以防止多调用拷贝构造影响效率,Date&
而后置是需要在函数内部实例化一个临时变量tmp,而我们直接对*this进行修改,最后将tmp返回,因为我们实现的是后置++,*this指向的内容需要更改,但是我们需要返回的是一个更改之前的值,所以我们需要备一份。但是对象tmp是临时的,出了函数的作用域就会被销毁,所以我们只能任其调用拷贝构造,所以返回类型不能为引用,Date
综上,++ / --(前置 / 后置)代码如下:
Date& Date::operator--()//前置
{
*this -= 1;
return *this;
}
Date Date::operator--(int)//后置
{
Date tmp = *this;
tmp -= 1;
return tmp;
}
Date& Date::operator++()//前置
{
*this += 1;
return *this;
}
Date Date::operator++(int)//后置
{
Date tmp = *this;
tmp += 1;
return tmp;
}
流相关,如<<、>>(cout、cin)
我们在C语言中曾学到了printf和scanf
我们都知道其用法,就是先指定类型,接着我们再将要打印的变量放上去
如果只是打印内置类型还好,但如果是打印自定义类型呢?我们没办法用printf打印自定义类型
本贾尼博士之所以整了一个cout、cin(流插入流提取),就是因为想像打印内置类型一样打印自定义类型的,并且cout可以自动判断要打印变量的类型
我们来看一张图:
如上我们可以看到,cout 和 cin 是两个实例化出来的全局对象,类的类型分别是 ostream、istream
而 ostream、istream 又被包在iostream里面
而我们如果想要实现可以像内置类型一样打印自定义类型的话,我们只需要自己实现一下operator<< 和 operator>> 即可
我们先不考虑连续打印的问题,返回值先设置为void
而我们的参数就传一个ostream&类型的对象即可,剩下还有一个this指针(这里埋一个坑)
代码如下:
void Date::operator<<(ostream& out)
{
out << _year << " " << _month << " " << _day << endl;
}
但是当我们想要调用的时候,却会发现调用不了
Date s1(2024, 1, 13);
cout<<s1;
你会发现调用不了,这是因为我们在调用的时候,要遵循的顺序是参数的顺序
看我们函数内实现的逻辑,我们的第一个参数是this指针,随后才是ostream
这就意味着如果按照上面写的函数的话,我们就需要将调用的顺序改一下我们才能调用成功
Date s1(2024, 1, 13);
s1<<cout;
如果还不理解的话,我们来显示调用一下我们的函数相信你就能看得懂了
s1.operator<<(cout)
综上,我们如果这么写的话与我们日常的使用逻辑相悖,所以我们需要做出相应的调整
但是this指针已经将第一个参数的位置给牢牢地占住了,我们没有办法做更改
所以我们需要将这个函数变为全局的
当这个函数编程全局的的时候,我们就能够自己定义参数的位置了
void operator<<(ostream& out, const Date& d)
但是如上我们就=还是不能正常调用
这是因为当我们将这个函数变为全局函数的时候,这个函数就无法调用类里面的私有变量
为此,我们需要使用友元的方式进行解决
我们在这里先简单提一下友元的概念,具体的知识我们放到下一篇博客类和对象下中去讲
友元,就是说:我是你的朋友,所以你的东西就是我的东西,但是只是你把我当朋友,我还没有把你当朋友,所以你的东西我能用,但我的东西你不能用
要定义成友元也比较简单,我们只需将函数的声明在类中任意位置放一份,并在最前面加上一个friend即可,如下:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
.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();
int GetMonthDay(int year, int month)
{
assert(month < 13 && month > 0);
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 % 400 == 0))
return 29;
else
return MonthDayArray[month];
}
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);//*this要变,故不加const
Date operator+(int day)const;
Date& operator-=(int day);
Date operator-(int day)const;
int operator-(const Date& d);
Date& operator--();
Date operator--(int);
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);
所以我们的<<就变成了:
void operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
}
但是,如果我们遇到需要连续打印的情况,我们写的这个函数就不够用了,因为没有返回值
但是如果我们想要支持连续打印的话,我们需要将out传回去
所以,最终版本如下:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
接着我们来看一下 >>
这个也是一样的,我们的istream是一个类,我们同样将其设置为全局函数,最后通过友元的方式令其能够访问到私有变量,而为了支持连续赋值,所以我们同样需要一个返回值,返回值的类型就是istream&
代码如下:
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
结语
到这里我们的日期类就结束了
如果你觉得这篇博客对你有帮助的话,希望各位能够多多支持!!
(比心)