实现日期类
说明
实现日期类的目的是为了让我们对前面的知识点有更加深入的理解,日期类的实现我们统一采用声明和定义分离的方式,声明写在Date.h文件中,函数定义写在Date.cpp中。
1、日期类的成员变量
日期类的成员变量就三个,年:year,月:month,日:day。为了区分类内成员变量和函数参数我们统一在前面加上_。
// Date.h
class Date
{
public:
private:
int _year;
int _month;
int _day;
};
2、Print函数
用来打印输出日期,方便我们进行调试,不对日期对象进行修改,可以加上const。
(当然我们后面还会重载<<和>>操作符)
// Date.h
void Print() const;
// Date.cpp
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
3、构造函数
日期类的构造函数我们可以给成全缺省,这样方便我们进行初始化。因为输入的日期可能是非法日期,所以我们还需要对输入的日期进行判断,我们可以实现一个GetMonthDay接口,来获取对应月份的天数进行判断。
由于GetMonthDay只是对月份天数进行判断,并不进行值的修改,我们可以加上const,而构造函数在.h声明中给出全缺省,在.cpp中就不需要给出缺省值了。
// Date.h
int GetMonthDay(int year, int month) const;
Date(int year = 1, int month = 1, int day = 1);
// Date.cpp
int Date::GetMonthDay(int year, int month) const
{
static int monthDay[13] = {
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = monthDay[month];
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day++;
}
return day;
}
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!(_year >= 0
&& (_month > 0 && _month < 13)
&& (_day > 0 && _day < GetMonthDay(year, month))) )
{
cout << "非法日期:";
// 调用下面的打印日期函数
Print();
}
}
monthDay数组我们给出13个空间,1-12则对应每个月份的天数。需要注意闰年的2月份是29天,所以我们需要对月份和年份进行判断。
另外日期类的析构函数由于没有资源需要释放,所以可以不写。拷贝构造和赋值运算符重载也可以不写,因为默认的浅拷贝就可以。
4、六个比较函数
日期类是可以进行比较的,所以我们可以重载<、==、<=、>、>=、!=这六个操作符。而我们不必六个函数都实现一遍,只需要实现<、==或>、==即可,剩下四个直接复用。这六个操作符都是比较,不进行值的修改,所以都可以加上const。
// Date.h
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.cpp
bool Date::operator<(const Date& d) const
{
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) const
{
if (_year == d._year &&
_month == d._month &&
_day == d._day)
{
return true;
}
return false;
}
bool Date::operator>(const Date& d) const
{
return !(*this < d || *this == d);
}
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);
}
然后我们可以测试一下:
// Test.cpp
#include "Date.h"
using namespace std;
int main()
{
Date d1(2024, 4, 19);
Date d2(2024, 5, 20);
Date d3(2024, 4, 19);
cout << (d1 < d2) << endl; // 1
cout << (d1 == d3) << endl; // 1
cout << (d1 <= d3) << endl; // 1
cout << (d1 != d2) << endl; // 1
return 0;
}
5、重载operator+=和operator+
日期加日期是没有意义的,但是日期加天数是有意义,可以帮我们计算几天后是几月几号,所以我们可以重载operator+=和operator+。而operator+=是需要修改日期本身的,operator+是不需要修改日期本身的,operator+是在原日期的基础上返回一个+n天之后的日期,所以我们可以先实现+=,然后在+中复用+=。
// Date.h
Date& operator+=(int day);
Date operator+(int day) const;
// Date.cpp
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
首先考虑普通情况,当day是正数,我们直接加到_day,然后循环判断天数是否大于当月的天数,如果大于当月天数就减去当月天数,然后让_month++,同时判断_month是否加到了13,加到13说明应该让年份加1,修改月份为1月份。
另一种情况就是day是负数,+=一个负数,说明应该让日期倒退,我们直接复用-=即可。(-=在后面实现)
接下来实现operator+,这并不需要直接修改对象本身,所以我们直接拷贝构造一个新对象,然后让这个新对象复用+=返回即可。
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
6、重载operator-=和operator-
这里同样也是让-复用-=,-=里面同样要判断day为负数的情况。先减去day,然后循环判断当_day < 0时,每次先让_month–,退回上一个月,同时要判断月份是否为0,如果为0说明要回到上一年的十二月份,然后让_day加上当前月份天数。
// Date.h
Date& operator-=(int day);
Date operator-(int day) const;
// Date.cpp
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
7、重载operator++
日期类是可以自增的,这里又分为前置++和后置++,前置++就是先让日期类自增一天,然后返回。后置++为了跟前置++区别我们添加一个int参数构成函数重载,后置++我们可以用日期类对象拷贝构造一个新对象,让日期类对象自增,然后返回这个新对象即可。同样的,这里的++可以复用前面的+=。
// Date.h
Date& operator++();
Date operator++(int);
// Date.cpp
Date& Date::operator++()
{
*this += 1;
return *this;
}
//增加一个int参数占位,跟前置++构成函数重载
Date Date::operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
8、重载operator–
这里基本上同operator++,就不再赘述了。
// Date.h
Date& operator--();
Date operator--(int);
// Date.cpp
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
9、重载operator-(日期-日期)
前面我们重载的operator-是日期减去天数,我们还可以重载一个日期减日期,这样可以帮我们计算两个日期之间间隔多少天。
这个实现其实不难,我们首先定义两个日期类对象max和min,然后把大的赋值给max,小的赋值给min,由于日期减去日期可能算出负数,所以需要一个flag来对结果进行修正。用一个计数器count来计算天数,循环判断min != max,每次让min++,count也++。最后返回count * flag就是对应的天数了。
// Date.h
int operator-(const Date& d) const;
// Date.cpp
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (min != max)
{
++min;
++count;
}
return count * flag;
}
10、PrintWeekDay函数
实现该函数的目的是为了计算出当前日期是周几。
这里有一个技巧,1900年1月1号是星期一,所以我们直接让当前日期减去1900年1月1号,计算出中间间隔了多少天,然后模7,余数0-6对应的就是星期一到星期日了。
// Date.h
void PrintWeekDay() const;
// Date.cpp
void Date::PrintWeekDay() const
{
const char* arr[] = {
"星期一", "星期二", "星期三", "星期四",
" 星期五", "星期六", "星期天" };
int count = *this - Date(1900, 1, 1);
cout << arr[count % 7] << endl;
}
补充:这里Date(1900, 1, 1)是一个匿名对象,该匿名对象的生命周期仅在本行,当该行代码执行完毕后,匿名对象也就不复存在了。
11、重载<<(流插入)和>>(流提取)运算符
为什么要重载流插入和流提取呢。
对于内置数据类型,我们读取数据和打印数据是通过cin >> x; cout << x << endl;来实现的。
那么针对自定义类型,我们也可以重载operaotr<<和operator>>来实现对自定义类型的数据读取和输出。
前面我们讲了,cout和cin会自动识别类型,这是为什么呢?
这是因为cin实际上是一个对象,他重载了operator>>,所以对于内置类型int、double、char等都可以使用。
同理,cout也是一样的。
那么我们就开始实现流插入和流提取运算符的重载。
首先我们要知道cin实际上是istream类的对象,cout实际上是ostream类的对象,所以返回值必须是istream&和ostram&。之所以返回引用是为了支持连续的读取和输出。
例如连续的读入:cin >> x >> y;如果不返回对象的引用,那么就无法连续的读入数据。
同理为了支持连续的输出数据,需要返回ostream对象的引用
// Date.h
istream& operator>>(istream& in);
ostream& operator<<(ostream& out) const;
// Date.cpp
istream& Date::operator>>(istream& in)
{
cout << "请依次输入年月日,以空格间隔:>";
in >> _year >> _month >> _day;
return in;
}
ostream& Date::operator<<(ostream& out) const
{
out << _year << "-" << _month << "-" << _day;
return out;
}
代码写完后,我们进行测试,发现报错了?
这是为什么呢?我们不是重载了operator<<和operator>>吗?
实际上这是因为我们重载的operator<<和operator>>参数位置是错误的。
修改写法后:可以看到可以通过编译,正常读入数据并且输出。
但是这种写法不就和我们正常使用反过来了吗?
我们正常的写法都是cin和cout在前面,这样写就很别扭。所以接下来我们就要引入友元函数了。
如果把流插入和流提取写在类内,就会导致隐藏的this指针占用第一个参数的位置,而第一个参数的位置应该是istream或ostream对象的引用,所以我们要把operator<<和operator>>写在类外,重载成全局函数。
// Date.h
// 写在类外
istream& operator>>(istream& in, Date& d);
ostream& operator<<(ostream& out, const Date& d);
// Date.cpp
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日,以空格间隔:>";
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
但是这里还有一个问题:因为我们定义的成员变量_year、_month、_day是私有的,在类外不能访问,而我们重载的operator<<和operator>>是写在类外的,所以编译报错。
所以我们需要让operator<<和operator>>成为Date类的友元函数,具体做法就是:friend+函数声明,如下图:
如图,可以正常的使用cin来读取对象数据,cout输出对象数据了。