目录
前言
前面我们已经学了类和对象上、中两个模块的内容了,我们可以综合学习的这些内容来实现一个日期类。
- 日期类的实现我们用3个文件来实现,分别是,Date.h Date.cpp test.cpp
- Date.h文件用于类中需要的成员函数的声明
- Date.cpp文件用于成员函数的实现
- test.cpp文件用于用户测试案列
- 下面是Date.h的内容
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d); //友元函数
friend istream& operator>>(istream& in, Date& d); //友元函数解决类外面函数访问私有变量
public:
Date(int year = 1900, int month = 1, int day = 1); //构造函数
bool CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
void Print();
int GetMonthDay(int year, int month) //类里面的函数定义默认内联
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //高频调用static防止每次调用重复开辟空间
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
return monthDayArray[month];
}
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);
bool operator!=(const Date& d);
Date& operator+=(int day); //日期+天数
Date operator+(int day);
Date& operator-=(int day); //日期-天数
Date operator-(int day);
Date& operator++(); //前缀加加
Date operator++(int);//后缀加加(int只是为了函数重载)
Date& operator--(); //前缀减减
Date operator--(int);//后缀减减
int operator-(const Date& d);//日期减日期,得到天数
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d); //流插入运算符重载,返回值是为了连续插入
istream& operator>>(istream& in, Date& d); //这里不const,d要改变
我们现在把这些函数分为四个模块
- 默认成员函数模块
- 关系运算符重载
- 日期计算的相关运算符重载
- 实现流插入<<和流提取>>对日期进行输入输出
思路及代码实现
【第一模块】 默认成员函数的实现
构造函数
- 对于一个类来说,系统自动默认生成的构造函数不会对内置成员变量初始化,我们通常都有一个默认日期,所以我们需要自己写一个构造函数。
- 这里推荐使用全缺省参数构造,方便以后修改默认值。
Date(int y = 2000, int m = 1, int d = 1);
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
- 但是我们设置默认日期的时候不小心手滑了
- 2023年不是闰年吧?不是闰年2月就没有29天
- 对于这种判断日期是否合法的情况,我们不妨直接封装一个函数,来判断一下是否合法,万一后面也用得到呢?
1.一个日期是否合法是不是应该判断月份。(年份还有公元前这些,直接忽略)
if(_month < 1 || _month > 12)
{
return false;
}
2.还有就是要判断每个月的天数是否合法,比如只有闰年的2月才有29天
-
那么现在又有一个问题就是怎么获取每个月的天数
-
这里我们用封装一个函数,我们为函数提供年份和月份,函数里面实现一个数组,数组下标是从0开始,0这个位置用-1占为,然后1-12个下标存储1-12个月的天数,然后根据我们提供的月份返回对于月份下标元素。如果是闰年将为2下标元素+1.
int GetMonthDay(int year, int month) //类里面的函数定义默认内联
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //高频调用static防止每次调用重复开辟空间
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
return monthDayArray[month];
}
-
这个函数我们就在类里面定义实现,因为他代码短小简单,而且经常被调用,类里面的函数默认内联。内联就不用开辟函数栈帧,提升执行效率
-
实现了这个函数后,我们就可以判断天数了。
bool CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
-
这样就实现了日期是否合法函数,这样的函数也是经常被调用,不妨直接在类里面实现,默认内联。
-
这时候我们再去实现刚刚的操作
拷贝构造&&析构函数&&赋值运算符重载
对于拷贝构造函数
- 如果我们没有显式写拷贝构造函数,那么拷贝构造函数会调用编译器自动生成的默认拷贝构造函数,这个默认生成的拷贝构造函数,对内置类型成员变量会浅拷贝。对于自定义类型成员变量会调用他的默认拷贝构造函数。
- 所以如同日期类这样不需要动态申请资源。的类,我们通常不需要显式写拷贝构造函数来完成深拷贝。
- 深拷贝和浅拷贝我已经在类和对象(中)讲过,如果不知道可以去参考一下。
对于析构函数
- 析构函数是一样的,我们如果没有显示动态申请资源 这些,我们可以不写析构,销毁对象的时候会直接把这块空间销毁。
对于赋值运算符重载
- 和拷贝构造一样,如果我们没有显式写赋值重载函数,那么会调用编译器自动生成的赋值重载函数,这个默认生成的赋值重载函数,对内置类型成员变量会浅拷贝。对于自定义类型成员变量会调用他的默认赋值重载函数。
所以,我们日期类通常不需要显示写拷贝构造和析构函数,用编译器自动生成的就行了。
【第二模块】关系运算符重载
日期相等
bool Date:: operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
- 这就不用说了吧,年月日都相等就行了。
- 这里为什么_year是this指针,不知道可以看看前面的运算符重载
日期<日期
bool Date:: operator<(const Date& d)
{
if (_year < d._year) //如果年份小于,那么一定小于
{
return true;
}
else if (_month < d._month && _year == d._year )// 为了省去考虑月份小于,但是年份不小于的情况,这里只判断年份相关情况月份小于
{
return true;
}
else if (_day < d._day && _year == d._year && _month == d._month )//和月份一样思路
{
return true;
}
return false;
}
知识中转站
知道为什么上面先写小于和等于吗,看看下面的运算符复用就知道了
日期不等于日期
bool Date:: operator!=(const Date& d)
{
return !(*this == d);
}
- 看到了吧,直接对两个日期相等的情况取反,相等true变成了false,不相等false编程了true.这就不符合我们的不等于规则吗。
日期小于等于日期
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);
}
【第三模块】日期计算相关运算符重载
前置++
- 对于自定义类型前置++,和普通内置类型变量一样+整数。
int i = 1;
++i;
//最终i 为2
- 注意的是他++的同时改变了变量自身,也就是++马上执行。如果用变量接受,就是++后的结果
int i = 1;
int j = ++i;
//此时j为2Date& Date:: operator++()
- 此时我们运算符重载返回的就是 已经自增的对象。
Date& Date:: operator++()
{
return *this += 1;
}
- 因为是自己会实现递增,所以使用this即可,最后要给到++之后的返回值,那便返回this即可,那返回一个出了作用域不会销毁的成员,我们可以使用引用返回减少临时拷贝
后置++
- 相比于前置++,在执行该语句的时候表达式的值没有改变,我们需要返回一个没改变的值。但是当该语句执行完的时候,表达式的值还是改变了。
int i = 1;
int j = i++;
//此时j这条语句的值是1,但是当这条语句执行完后,i++这个表达式的值为2
{
return *this += 1;
}
- 此时我们运算符重载函数返回的就是自增之前的值
Date Date:: operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
- 这里运算符重载函数形参int无意义,只是为了区别前置和后置的运算符重载。
- 还有就是为什么前置的返回值类型Date&,后置返回值类型Date,我们这个日期如果在出了这个函数不销毁对象那么就可以引用返回(减少拷贝),而这个后置++的tmp对象是出了函数是销毁的所以是函数值返回。
关于this和*this区别
有些细心的同学知道,this是指针变量,*this是对象本身。
但是我们在复用的时候出现
Date& Date:: operator++()
{
return *this += 1;
}
- 有人说operator+=是这样定义
Date& Date:: operator+=(Date* this, int day)
- 这样形参和实参的接受类型不一样啊,形参*this是对象本身,而实参需要一个地址,或一个一级指针变量。
- 但是我想说的是,这里*this不是形参而是+=运算符重载函数的调用(*this).operato+=r(this, 1)
- 这样就是当前对象调用operator+=函数提供当前一级指针变量和天数。
- 直接*this += 1是符合我们的习惯,但是编译器底层是上面的这样。
前置自减
- 道理和前置++一样
Date& Date::operator--()
{
return *this -= 1;
}
后置自减
- 道理和后置++一样
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
日期+=天数
- 首先来看看函数声明,形参部分就是需要传入改变的天数
Date& Date:: operator +=(int day)
- 然后我来说一下这里的算法
比如说我们有一个日期是2024/8/21,那么我们直接加上去100天,也就是_day+=100
这里就是this指针的_day,日期就变为了2024/8/121,很明显这样的日期是不合法的。如果我们的天数大于当前月份的天数,那么就减去当前月份的天数。并且月份+1,。当前月份的天数就用GetMonthDay函数来获取。还要注意的一点是,如果月份大于12这个临界值,那么年份+1,并且月份又从1月份开始计算。循环下去,直到天数小于或等于当前月份天数即可。
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 > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
- 还要注意的是,如果我们是+=一个负数,相当于就是-=一个正数,那么我们就直接调用-=然后堆day负负得正即可
日期+天数
-
注意哈,这里只是+,可不是+=。
-
+强调的是两个数+之后的返回值,所以通常只需要返回他们相加后的值。不在乎他们相加后还是否存在,所以通常是值返回。
-
+=强调的是这个对象+=后还存在,所以通常是引用返回。
Date Date:: operator +(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
- 此时tmp出了函数后就销毁了,但是强调的相加的值返回了。
日期-=天数
- 与上面的+=天数很类似的逻辑
- 假设我们的日期是2024/8/21,如果减去10天,就是2024/8/11,此时天数为正没有问题。
- 但是如果我们就去的30天,就是2024/8/-11,这时候天数为负就有问题了。
- 此时我们就需要向前一个月份7借位,将7月的天数+上去,此时就是2024/7/22.天数为正就没问题了
- 还要注意的是就是如果月份<1这个临界值的话,年份-1,并且将月份重新设置12.说明这一年的月份借完了,只能借上一年的了。
- 就这么循环往复,直到天数_day>0停止
Date& Date:: operator -=(int day)
{
if (day < 0)
{
return (*this) += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;//向上一个月份借天数
if (_month < 1)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
- 这里如果-=天数是负数,相当于+=一个正数天数,所以我们直接复用+=,然后负负得正天数。
日期-天数
- 那对于-也是一样,复用一下-=即可
Date Date:: operator -(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
日期-日期
- 讲算法之前先理清楚一个概念。
- 2024/8/20 和2024/8/23.是不是相差3天,小日期就是d1,大日期就是d2.所以d1-d2值是一个正数。小日期-大日期就是正数
- 反过来d2-d1,就是一个负数,即-3。也就是d2-d1是-3.。大日期减小日期就是负数
也就是说,我们需要判断this指针和d谁是小日期,谁是大日期。
- 我们假设this指针是小日期,d是大日期。用一个标识变量flag = 1
- 如果下面比较两个日期,this指针是大日期,d是小日期,那么this指针和d互换,flag=-1.这里就是大日期-小日期
- 然后我们从小日期开始自增计数天数,直到两个日期相等。
- 此时让day天数*flag,就是一个负数。满足了大日期-小日期是负数的情况。
int Date:: operator-(const Date& d)
{
int day = 0;
int flag = 1;
Date min = *this;
Date max = d;
if (*this > d)
{
max = *this;
min = d;
flag = -1;
}
while (min != max)
{
min++;
day++;
}
return day * flag;
}
【第四模块】流插入和流提取的实现
流插入
- 可以看到,在上面对日期进行各种操作后,我都调用了Print()函数打印观察日期的变化情况,虽然调用一下也不用耗费多少时间,但总觉得还是有些麻烦了,如果可以像正常变量一样直接用cout << a输出该多好
- 但是这个去输出一下就可以发现编译器报出了没有与这些操作数匹配的"<<"运算符
- 这是因为C++只对内置类型的对象重载了<<,对自定义类型的<<没有重载。这时候我们就需要自己写一个函数来重载<<用来使用自定义类型的对象。
void Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
}
- 这里ostream和istream类型是<<和>>运算符的类型,而这些内容封装在std命名空间.
- 但编译器还是报出了一些错误,那有的同学就感到很疑惑了??
- 但是当我把输出的表达式写成下面这样时,却出现了神奇的现象,居然不报错了!!
- 对于上面讲到过的内置类型重载,其实可以写成下面这样,以一种函数调用的方式.这一点我在前面this和*this的区别也说过。
- 那对于自定义的日期类型来说,其实应该写成下面这样子
- 可是这样写,不符合我们日常使用的习惯。怎么办呢?
- 是不是我们需要调整两个变量的位置?但是在类中的成员函数第一个参数默认必须是this指针。这时候我们就把函数不放在类中,放在全局中。
- 这时候问题又来了,放在全局中,我们就无法访问类的私有变量了
- 这里我们推荐使用友元函数来解决。
- 可以看到,只需在最前面加上一个friend做为修饰,就可在类内声明一下这个函数为当前类的友元函数
friend void operator<<(ostream& out, const Date& d);
- 此时再去尝试cout << d1就可以发现可以正常运行了
- 如果要实现连续赋值的话就要做一个当前对象的返回值,那这里我们若是要继续实现流插入的话就要返回一个ostream的对象才可以,那也就是把这个【out】返回即可,便可以实现多次流插入了
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
流提取`
- 也是和流插入一样的两块地方,在类内首先声明一下这个函数是当前Date类的友元函数,然后将ostream转换为istream输入流即可
friend istream& operator>>(istream& in, Date& d);
- 这里要注意的一点是形参的对象不可以用const来进行又是,不然它就具有常属性,我们无法往里面去写入东西
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请输入日期:>"<<endl;
in >> d._year >> d._month >> d._day;
if (d.CheckDate())
{
break;
}
else
{
cout << "日期不合法->";
cout << d;
}
}
}
- 这里注意判断输入日期的合法性