文章目录
- 前言
- 1. 日期的合法性判断
- 2. 日期+天数(+/+=)
- 2.1 +和+=的重载
- 2.2 对于两者复用的讨论
- 3. 前置++和后置++重载
- 4. 日期-天数(-/-=)
- 5. 前置- -和后置- -的重载
- 6. 日期-日期
- 7. 流插入<<重载
- 8. 流提取>>重载
- 9. 总结
- 10. 源码展示
前言
在上一篇文章我们学习类和对象的过程中,我们不是写了一个日期类嘛。
但是我们之前实现的日期类并不是很完整,我们只是借助它来帮大家学习类和对象的知识。
那这篇文章呢,我们就在之前的基础上,再增添一些功能,实现一个比较完整的日期类,作为一个练习,来帮助我们更好的理解我们之前学过的知识。
这是我们之前一起写的不太完整的日期类:
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator<(const Date& d)
{
if (_year < d._year)
return true;
else if (_year == d._year && _month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
return false;
}
bool operator<=(const Date& d)
{
return *this == d || *this < d;
}
bool operator>(const Date& d)
{
return !(*this <= d);
}
bool operator>=(const Date& d)
{
return !(*this < d);
}
bool operator!=(const Date& d)
{
return !(*this == d);
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//赋值重载(对于Date类用默认生成的就行)
//d1=d2(this就是d1,d就是d2)
/*Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}*/
private:
int _year;
int _month;
int _day;
};
那这些实现过的函数我们就不再一一讲解了,大家不熟悉的话可以回顾一下上一篇文章。
另外呢,我们最终实现的是一个完整的日期类,那方便对代码进行维护和管理,以及对实现好的日期类进行测试,我们还是像之前写数据结构一样,放在多个文件中。
1. 日期的合法性判断
我们之前呢已经给该日期类写好了构造函数:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
并且还指定了缺省参数,那这样的话在实例化一个对象时我们就可以自己给对象指定初值,我们输入的日期是啥,该对象就会被初始化为对应的日期。
那现在有一个问题,如果我们实例化对象时给的日期不合法呢?比如:
void Datetest1()
{
Date d1(2023, 2, 30);
d1.Print();
}
不合法是不是也判断不出来啊。
那我们就对原来的构造函数做一些补充好吧,让它在给对象初始化的时候可以去判断一下对应的日期合不合法。
那要怎么判断呢?
给我们一个年月日,要判断是否合法,是不是要判断月在不在【1,12】之内以及天数有没有超过当前月的总天数啊。
但是某个月的天数是不是不好确定啊,不同月的天数不一样,而且要考虑平闰年。
那我们先来写一个获取某年某月天数的函数:
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int MonthArr[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 MonthArr[month];
}
}
不过多解释了,相信大家都能看懂。
那有了这个函数,我们就可以在构造函数中去判断了:
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;
_year = 1;
_month = 1;
_day = 1;
}
}
如果合法了,我们再去赋值,不合法就还全搞成1,要不然就是随机值了。
那这时我们再用非法日期去初始化对象:
这样输入的日期不合法就提示了。
2. 日期+天数(+/+=)
我们说日期+日期是不是没啥意义啊,但是日期+一个天数是不是还有点意义,可以理解成这么多天之后是几几年几月几日。
2.1 +和+=的重载
所以接下来,我们要实现一个功能就是计算一个日期加了一个天数之后得到的日期:
那具体实现的思路呢可以这样搞:
首先我们想让自定义类型Date的对象直接和整型相加,这里肯定要对+
进行运算符重载。
我们拿到一个日期一个天数之后,先把天数加到日上,然后,判断此时的日是否超过了当前月的总天数(获取月份天数的函数我们之前已经实现过了),超过的话,就减去当前月的天数,然后月份++,那月份++有可能会超过12啊,一旦超过,就让年++,然后月置成1。
那当然这肯定是一个循环,当我们的日(day)不再大于当前月的天数,则此时得到的就是最终的日期。
我们来写一下代码:
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;
}
这里*this出了作用域还在,所以可以返回引用。
那我们来测试一下:
我们看这个计算出来的日期确实是没问题的,但是d1+100,这里是+而不是+=,所以d1是不是不应该变啊,我们只是把d1+100得到的日期赋给了d2,但是现在d1也变了。
所以我们这样写其实实现的是啥,是不是+=啊。
所以我们这里重载的应该是+=:
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;
}
那+=还需要有返回值吗,🆗,最好还是带返回值,带返回值的话就可以支持这种情况:
d2 = d1 += 100;
那刚才实现的是+=,那+要这么搞?
是不是借助一个临时对象就可以搞定了啊,我们只要不改变*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;
}
那就这样,测试一下:
🆗,这次d1没有被改变。
答案也没问题。
那对于+的重载:
大家有没有注意到我们没有返回引用,为什么?
因为我们返回的是啥,是不是tmp,而tmp是一个局部的对象,出了作用域就销毁了,所以我们不能返回引用。
那有时候呢,有的人为了这个地方能够返回引用,对tmp加了一个static
修饰,然后就返回引用。
大家思考一下这样可以吗?
我们试一下会发现:
第一次没问题,但我们第二次在调用+,是不是就错了啊。
第二次我们还是给d1+1000天,但结果却是2000天之后的。
为什么会这样?
原因就在于我们用了static。
我们创建的局部对象tmp被static修饰后,就不存在栈区了,而是放在静态区了,所以静态局部变量出作用域不会被销毁,而是保留在静态区,因此我们确实可以返回引用了。
但是,静态局部变量的初值是在编译期间就指定的,所以运行期间,不管我们再去调用几次函数,tmp都不会被重新初始化,而是保留上次的值,所以我们第二次的结果才会变成2000天之后。
之前有篇文章详解static的作用,大家可以看——链接: link
所以呢,这个地方我们不要为了能够返回引用而去随便加static。
2.2 对于两者复用的讨论
那除此之外:
大家看,+的重载与+=的重载,除了多一个临时的局部对象,其它的逻辑是不是一样啊,所以+里面是不是可以复用+=啊。
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
这样是不是就行了啊:
那既然+可以复用+=,我们是不是也可以考虑先实现+,然后+=复用+呢?
当然可以。
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;
}
那这是我们自己写的+,现在我们+=来复用+。
Date& Date::operator+=(int day)
{
*this = *this + day;
return *this;
}
这样不就行了。
那再来思考:
到底是+复用+=好呢,还是+=复用+好呢?
🆗,那这里呢,其实是我们的第一种即+复用+=更好一点。
因为+里面创建临时对象有一次拷贝,返回的是值而不是引用又是一次拷贝。
那如果是+复用+=的话就只有+里有拷贝,但如果是+=复用+的话,是不是+=和+里都有拷贝了。
3. 前置++和后置++重载
刚重载了+和+=,那是不是还有前置++和后置++啊,那我们也来实现一下。
先来前置++吧:
来回忆一下前置++的特性是什么?
是不是先++,后使用啊,即返回的是++之后的值。
举个例子:
int a=5;
int b=++a;
首先不管前置++,还是后置++,a的值肯定都会+1,那现在
++a
前置,先++后使用,返回++之后的值,所以b也是6。
那我们来重载一下:
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
很简单,直接复用+=。
那我们再来重载后置++:
后置++呢,是先使用,后++,即返回++之前的值。那我们还是可以借助一个临时对象。
但是呢?
这里会发现有一个问题:
前置++和后置++没法区分啊,它们的参数和函数名是不是一样啊。
欸,那这怎么解决啊?
🆗,那当然是有办法的。
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递(它的作用就是为了构成重载),编译器自动传递。
所以呢,这样搞就行了。
我们来实现一下:
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
🆗,我们就搞定了。
那实现完了,大家看一下:
前置++和后置++那个效率会高一点。
是不是前置啊,因为与后置相比,前置没有拷贝啊对不对。
所以,对于内置类型来说,大家可以认为前置后置没有区别;但是对于自定义类型来说,能用前置,尽量用前置++。
我们的前置++和后置++就搞定了,那在调用的地方呢,我们正常去用就行了,编译器会自动识别匹配。
遇到前置++,它就会自动去调用前置++;遇到后置++,编译器会自动传一个整型参数,去调用后置++。
4. 日期-天数(-/-=)
上面我们通过重载+进而实现计算一个日期+一个天数之后是什么日期,那是不是还可以搞一下日期-天数,计算它的前多少天是什么日期。
那我们先来重载-=:
那思路和上面加一个天数也是类似的,我们先用日减去传过来的天数,如果减完之后<=0,说明当前日期是不合法的,那怎么办,就去加上一个月的天数,然后月–,当然要注意判断会不会减到上一年,然后还<=0的话,就继续,知道日>0循环结束。
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
测试一下:
没问题。
然后我们再重载一下-:
那是不是简单啊,直接复用-=。
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
测试 一下:
🆗,看起来好像没什么问题了,但是,如果这样呢:
这里减一个负数,会发现结果就错了。
为什么呢,大家可以调试观察:
会发现这里根本不会进入循环,直接就返回了。
所以针对负数的情况,我们要单独处理一下:
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
我们-是复用-=的,所以这里在-=里面处理。
-=一个-100就相当于+=一个100嘛。
那同样对于+和+=我们也要处理一下:
那我们来处理一下:
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
这样是不是就行了,+=一个-100就相当于-=一个100嘛。
5. 前置- -和后置- -的重载
那有了上面的练习,再实现前置- -和后置- -不是soeasy嘛。
前置- -:
Date& Date::operator--()
{
*this -= 1;
return *this;
}
先- -,后使用,返回- -之后的值。
后置- -:
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
先使用,后- -,返回- -之前的值。
6. 日期-日期
上面我们搞了日期加减天数,那两个日期相减是不是也有意义啊,可以得出这两个日期之间差了多少天。
那如何计算两个日期之间相差的天数呢?
🆗,那这里给大家提供一种比较好实现的思路:
我们拿到两个日期之后,先把较小的那个日期找出来,然后用一个计数器,统计较小日期++的次数,当两个日期相等时停止,++的次数就是相差的天数。
另外如果是第一个日期大于第二个,结果应该是整数;如果第二个大于第一个,应该是负数。
我们来实现一下:
//日期-日期
int Date::operator-(Date d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
没有问题。
7. 流插入<<重载
那我们现在打印一个日期类对象的时候是不是都是去调用我们写的
使用我们之前学的cout
+<<
去打印。
大家记不记得:
我们之前文章里学习C++输入输出的时候,其实只是浅浅的提了一下如何去用,并没有对cout、<<和>>进行过多讲解。
因为以我们那时的知识储备还不能很好的理解:
那我们现在就可以再尝试多理解一点东西:
我们发现我们的自定义类型想用这些运算符是不是都要重载啊,除了赋值运算符以及取地址运算符,因为赋值重载和取地址重载是类的6个默认成员函数之中的,我们不写编译器会自动生成一个。但是在有些情况下根据实际情况我们还是需要自己写。
🆗,这是我们上一篇学习的知识,不用过多说明了。
然后呢我们还说C++里这样输入输出可以自动识别类型。
那为啥可以自动识别类型,其实是因为它这里对
<<
进行了函数重载。
为什么我们的内置类型可以直接用,因为库里面已经对这些类型都进行了重载,所以可以自动识别类型,是根据操作数的不同类型进行匹配。
那我们现在自定义类型想用怎么办,是不是需要自己重载啊。
那我们接下来就重载一下<<
好吧:
void operator<<(ostream& out);
那想用
<<
是不是得使用cout
,<<
两个操作数,一个cout,一个是我们要打印得对象,cout
是啥?
🆗,上面说了它是一个ostream
类型得对象,所以我们把cout
作为参数传过去。
那这里out就用来接收cout,this指针指向得就是我们要打印得类对象。
来实现一下:
void Date::operator<<(ostream& out)
{
out << _year << _month << _day << endl;
}
这样是不是就行了啊,它的成员变量都是内置类型,我们就可以使用<<直接打印了。
我们测试一下:
但是我们使用的时候发现报错了。
这里操作数是不是写反了,为什么?
对于函数重载来说,两个参数的话,第一个参数是左操作数,第二个参数是右操作数。
但是对于类成员函数来说,第一个参数是不是隐藏的this指针啊,它指向我们调用该函数的对象,所以这里第一个参数是Date对象的地址。
那我们要调用的话,应该这样写:
但是这样写是不是好别扭啊。
怎么让它像内置类型那样使用啊。
那我们就要想办法让cout成为第一个参数,怎么做呢?
把函数重载写到类外面不就行了是吧。
没有说运算符重载不能写到类外面啊,我们写到类里面是为了啥,是不是只是为了可以访问private修饰的成员变量啊
那我们现在重载到外面:
那现在面临的问题是啥?
是在类外不能访问私有的成员变量,那怎么办?
可以把成员变量变成共有的public,但这样是不是就不能很好的保证封装性了;
或者可以提供Get方法,但C++一般不喜欢这样搞。
那还有什么方法呢?
🆗,就是用友元函数。
那怎么做呢?
我们把函数声明再放到类里面一份,但在前面加一个friend就行了:
那这下我们就可以正常使用<<了:
但是呢:
这里不支持连续的流插入。为什么呢?
因为我们重载的没有返回值
那应该返回啥?
是不是返回cout呀,让它能够继续打印后面的。
我们看其实库里面就是这么搞的:
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
这样就行了。
8. 流提取>>重载
那紧接着,我们再来重载一下流提取:
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
测试一下:
就完成了。
9. 总结
那最后呢,还要给大家说明一点:
我们在之前的类和对象第一篇其实就提到了:
就是类的成员函数如果直接定义在类里面,编译器是会将其当成内联函数的,不管你有没有加inline关键字。
那我们在学习内联函数的时候也说了:
一般建议将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数实现成内联函数。
所以说,类里面的成员函数如果规模较小,适合做内联函数的,直接定义在类里面。
不过我们今天写的这个日期类,里面我是所有成员函数的声明和定义都分离了,大家以后可以根据实际情况,有些成员函数直接定义在类里面。
但是我们说了内联函数不是只是对编译器的一个建议嘛,如果规模较大的函数就算我们实现成内联函数编译器也不一定按照内联函数处理。
那在类里面也是这样,就算我们把全部的类成员函数都直接定义在类里面,也不一定会全部当做内联函数处理,编译器也还是会看它具体适不适合做内联。
另外还有一点:
上一篇文章我们不是还学习了const成员函数嘛,大家还可以看一看我们日期类的这么多成员函数,哪一个在函数内部不需要改变调用它的对象,是不是把它实现成const成员函数也是比较好的。
10. 源码展示
最后,把完整的源码给大家展示一下:
- Date.h
#pragma once
#include <iostream>
#include <stdbool.h>
#include <assert.h>
using namespace std;
class Date
{
//友元
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
int GetMonthDay(int year, int month);
//构造函数
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造函数
//Date(const Date& d);
bool operator<(const Date& d);
bool operator<=(const Date& d);
bool operator>(const Date& d);
bool operator>=(const Date& d);
bool operator!=(const Date& d);
void Print();
bool operator==(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
Date& operator-=(int day);
Date operator-(int day);
Date& operator--();
Date operator--(int);
//日期-日期
int operator-(Date d);
//赋值重载(对于Date类用默认生成的就行)
//d1=d2(this就是d1,d就是d2)
/*Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}*/
private:
int _year;
int _month;
int _day;
};
//<<
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
- Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int MonthArr[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 MonthArr[month];
}
}
//构造函数
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;
_year = 1;
_month = 1;
_day = 1;
}
}
//拷贝构造函数
//Date::Date(const Date& d)
//{
// _year = d._year;
// _month = d._month;
// _day = d._day;
//}
bool Date::operator<(const Date& d)
{
if (_year < d._year)
return true;
else if (_year == d._year && _month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
return false;
}
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);
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//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;
//}
//
//Date& Date::operator+=(int day)
//{
// *this = *this + day;
// return *this;
//}
Date& Date::operator+=(int day)
{
if (day < 0)
{
*this -= -day;
return *this;
}
_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;
return tmp;
}
//前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += -day;
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
//日期-日期
int Date::operator-(Date d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
//<<
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
- Test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
void Datetest1()
{
Date d1(2023, 2, 15);
d1.Print();
Date d2 = d1 + (-100);
d2.Print();
/*Date d3 = ++d1;
d3.Print();*/
}
void Datetest2()
{
Date d1(2023, 2, 16);
d1.Print();
Date d2 = d1--;
d1.Print();
d2.Print();
}
void Datetest3()
{
Date d1;
cin >> d1;
cout << d1 << endl;
}
int main()
{
//Datetest3();
return 0;
}
🆗,那这篇文章就到这里,欢迎大家指正!!!
下一篇文章,我们会对类和对象再做一些补充和收尾!!!