一.基本框架
根据我们过去实现项目的方法,我们需要将声明与定义分离,同时还要实现测试代码与源代码分离,所以我们需要三个文件:
随后进行类的创建,基本成员函数的实现,以及测试代码的创建等框架。
随后进行框架的测试:
二.日期的比较
两个日期之间的比较方式有很多种:>、<、<=、>=、==、!=
这些就需要我们的赋值运算符构造函数出马了。
上篇文章我们已经知道的“>”的写法:
bool operator>(const Date& d) { if (_year > d._year) return true; else if (_year == d._year) { if (_month > d._month) return true; else if (_month == d._month) return _day > d._day; else return false; } else return false; }
但是就这一个的写法,就已经是很多,很麻烦的一段代码了,难道像这样的代码我们一共要写6个吗???当然不需要,我们要知道,这些符号之间都有两两互补的关系。
比如说,我们现在写出了“>”,那么“<=”不就是“>”取反吗:
bool Date::operator<=(const Date& d) { return !(*this > d); }
我们建议把“==”给写出来,因为它比较容易写:
bool Date::operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }
如此一来,“!=”的写法就会是:
bool Date::operator!=(const Date& d) { return !(*this == d); }
现在我们同时拥有了“>”和“==”,那么将两者结合自然就得到了“>=”:
bool Date::operator>=(const Date& d) { return *this > d || *this == d; }
这样是不是非常的简洁???其余符号的代码就不一一列举啦,详情请看最后的完整代码展示。
三.日期的加减运算
日期的加减是一个相对比较困难的运算,它不像数字的加减那样简单,因为不仅存在大小月的天数不一,而且每四年还会出现闰年的特殊情况,这样就会导致进位非常的麻烦。下面我们就来详细分享一下,如此复杂的日期运算,到底该怎么实现。
1.得到月的天数
首先很重要的一点就是我们要能够知道每个月都分别有多少天,同时还有2月这个特殊的月份,我们通过一个函数来实现:
int GetMonthdays(int year, int month) { assert(month > 0 && month < 13); int Monthdays[12] = { 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; } return Monthdays[month - 1]; }
首先要做的就是assert断言,防止月份输入错误,其次因为闰年是斯四年一次,所以我们默认情况下都是平年,通过数组来记录,能够方便获取。最后就是进行闰年二月的判断,如果2月,我们就去判断一下是否是闰年。
2.日期的加运算
我们之间搬出代码来讲:
Date& Date::operator+(int day) { _day += day; while(_day > GetMonthdays(_year, _month)) { _day -= GetMonthdays(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; }
依旧是使用赋值运算符构造函数,我们直接让_day加上我们要加的天数,随后进行判断,如果相加之后的天数大于当月的天数,就让_day减去该月的天数,剩下的自然就是下个月的天数,同时月份+1,如果月份+1后是13,那就需要向年进一,同时月份回到1。
之所以使用循环,是因为如果我要加100天,那向月的进位就不止1了,所以要循环往复的判断。
下面我们进行测试:
#include"Date.h" int main() { Date d1(2024, 2, 1); Date d2 = d1 + 30; d2.Print(); return 0; }
结果如下:
1+30 = 31,而2024年恰巧就是闰年,所以2月有29天,31 - 29 = 2,所以结果为2024/3/2。
但是这样的写法看似完美,但实际上存在一个很大的错误,来看代码:
#include"Date.h" int main() { Date d1(2024, 2, 1); Date d2 = d1 + 30; d2.Print(); d1.Print(); return 0; }
我们是让d2对象去接收d1对象的日期加上20天后的日期,但实际上:
d1对象的日期也发生了改变。
这个错误其实也是可以理解的,因为我们在函数中直接默认进行操作的就是d1的成员变量。而这样的运算,实际上是“+=”运算。
所以想要保证d1的成员变量不变,就必须创建一个临时变量来代替:
Date Date::operator+(int day) { Date tmp(*this); tmp._day += day; while (tmp._day > GetMonthdays(tmp._year, tmp._month)) { tmp._day -= GetMonthdays(tmp._year, tmp._month); tmp._month++; if (tmp._month == 13) { tmp._year++; tmp._month = 1; } } return tmp; }
创建临时变量,就用到了我们的拷贝构造函数,使用tmp临时变量代替d1对象进行操作。
值得注意的一点是,由于tmp是临时的变量,当这个函数结束时就不存在了,所以其作为返回值时,返回类型不能是引用。
再进行测试,结果如下:
3.日期的减运算
理解了加运算之后,减运算的写法相信小伙伴们都能够自己悟出来了。
唯一值得注意的是,日期没有0天:
//日期减等运算 Date& Date::operator-=(int day) { _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthdays(_year, _month); } return *this; }
这里我们先来实现一下“-=”运算,注意while循环的判断条件,因为_day不可能等于0。
如果当月剩余的天数不够用,就需要去借用上个月的天数继续减。结果如下:
那么问题来了,博主为什么要先实现“-=”呢 ???
下面我们就来看看“-”运算的实现:
//日期减运算 Date Date::operator-(int day) { Date tmp(*this); tmp -= day; return tmp; }
怎么样,有没有很震惊,为了不改变d1对象,我们确实创建了临时变量tmp,但是我们大可不必去再写像上边那样的一大长串代码,因为我们已经有“-=”运算了,所以我们直接让tmp去进行“-=”运算,就可以得到结果:
而我们前边实现过的加运算同样可以借用“+=”运算来写:
//日期加运算 Date Date::operator+(int day) { Date tmp(*this); tmp += day; return tmp; }
4.日期的++--运算
我们知道,“++”和“--”运算都有前置和后置两种方式,那么我们该怎么用构造函数去分别实现呢?
不管是前置还是后置,它们都会有++,那么我们使用赋值运算符重载函数,函数名该怎么写?难道也是一前一后???
并不是,实际上是使用函数重载来区分它们:
//前置++运算 Date& operator++(); //后置++运算 Date operator++(int);
对于后置++,给它一个int参数,但是该参数并不会使用,只是用作编译器的区分。
那么两个函数又该怎么实现呢???
要注意的是,前置++是先加1,再给值,而后置++是先给值,再++,所以后者就需要一个临时变量,我们同样借用一下“+=”函数:
//前置++运算 Date& Date::operator++() { *this += 1; return *this; } //后置++运算 Date Date::operator++(int) { Date tmp = *this; *this += 1; return tmp; }
再来进行测试:
如此便可以实现“++”的运算符重载。“--”与之类似,博主这里就不做展开讲解。
5.日期减日期
上边我们讲的日期减运算,是用日期去减去明确的天数得到一个新的日期。
那么现在如果想用一个日期减去另一个日期,计算两个日期之间有多少天,又该怎么搞呢???
这个事情看似复杂,实则代码写起来也挺简单,现在给大家一个思想:
先去比较两个日期谁大,然后我让小的一直去++,并计数,直到跟大的相等,计数的结果不就是两者的相差天数吗???
//日期-日期 int Date::operator-(const Date& d) { Date max = *this; Date min = d; int flag = 1; if (*this < d) { flag = -1; max = d; min = *this; } int n = 0; while (min < max) { min++; n++; } return n * flag; }
先默认前一个值为较大值,后一个为较小值,然后去比较,如果前一个实际上是较小值,则进行互换,同时创建一个flag,如果是大-小,结果即为整数,反之则赋值为-1,结果为负数,测试如下:
6.日期的输入输出
我们前边讲述的日期,都是我们在创建对象时就给定的数据,输出时也是用的Print函数。而且我们知道,cin和cout是无法直接输入输出自定义类型的数据的。
那现在我们就想先创建一个对象,然后通过cin和cout来输入输出数据,该如何实现呢???
首先我们要知道,cin是istream类型的对象,而cout是ostream类型的对象,那么我们就可以通过赋值运算符重载函数来重载“>>”和“<<”两个符号来实现:
//日期输出 void Date::operator<<(ostream& out) { out << _year << "年" << _month << "月" << _day << "日" << endl; }
但是这样的写法存在问题:
赋值运算符重载函数定义在类中作为成员函数时,其第一个参数就会是默认的隐藏的this参数,也就是d1,而cout则是第二个参数,这就导致我们调用函数时两个实参的顺序存在问题,如果将其改为d1<<cout,就能通过编译:
但是这显然不符合我们C++的使用规范,所以想要恢复顺序,就需要将此函数定义在类外,交换两个形参的位置:
但是这个时候又出现了问题,因为该函数在类外,而类的成员变量是私有的,我们不能使用:
又该如何解决这个问题呢?
这就需要用到关键字:friend,通过friend,将类外函数在类内进行友元声明,就可以啦:
但是此时还有一个问题,在C++中cout是支持同时输出多个变量的,但是我们定义的函数却不行:
这是因为按照从左到右的顺序,执行完cout<<d1之后,它们需要返回一个ostream类型的返回值来继续和d2一起作为参数去继续调用函数,所以需要将该函数的返回值类型替换为ostream并返回out:
//日期输出 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; }
首先就是返回值类型和参数类型为istream&,其次要注意参数d不能用const修饰,因为就是要给它输入值。
测试如下:
7.存在的问题
到这里呢,日期类的所有基本功能已经全部实现了,但是任然存在一个问题:
我们不小心将2月的天数传了个40,这怎么能允许呢,2月最多也就29天,40天怎么可能呢?但是发现d2还是按部就班的进行了“+”运算,这就会出现很大的问题。所以我们需要进行传入检查。
因为在构造函数和输入函数中都需要进行检查,所以我们需要一个创建一个函数:
//检查日期合法性 bool Date::CheckInvalid() { if (_year <= 0 || _month < 1 || _month > 12 || _day < 1 || _day > GetMonthdays(_year, _month)) return false; else return true; }
分别判断年,月,日是否都合法。
//初始化 Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; if (!CheckInvalid()) { cout << *this << "该日期非法" << endl; exit(-1); } }
构造函数中使用,若非法直接结束程序:
//日期输入 istream& operator>>(istream& in, Date& d) { while(1) { in >> d._year >> d._month >> d._day; if (!d.CheckInvalid()) cout << "输入的日期非法,请重新输入:" << endl; else break; } return in; }
输入函数中使用,若非法则重新输入:
总结
日期类的实现到这里就分享完啦,希望能够帮助小伙伴们更加深入的理解类的内部结构及其成员函数的操作实现。