?个人主页:Forcible Bug Maker
?专栏:C++
目录
前言
日期类
日期类实现地图
获取某年某月的天数:GetMonthDay
检查日期合法,构造函数,拷贝构造函数,赋值运算符重载及析构函数
日期类的+=day和+day
日期类的-=day和-day
前置++和后置++
前置--和后置--
比大小运算符的重载
日期-日期返回天数
暴力++法
直接相减法
const成员
流插入和流提取重载
友元(friend)
结语
前言
本篇主要内容:日期类的实现
上篇我们介绍了拷贝构造函数和赋值运算符重载两大类的默认成员函数,本篇将会介绍更多关于操作符重载的实例运用。日期类,是与日期相关的类,主要用于处理与日期和时间相关的操作。我们将在完善一个日期类的过程中加深对运算符重载的理解和运用。在理解操作符重载之后,最后两个默认成员函数学习起来也就不是什么大问题了。
日期类
日期类实现地图
在实现日期类之前,需事先要知道要实现哪些内容。我会给出一份类的指南,也就是成员变量和成员函数的声明,然后根据声明一步步实现其中的成员函数。在代码编写过程中,成员函数是可以直接定义在类内部的;但是在实际开发过程中,考虑到工程级项目的规模,一般采用声明和定义分离的方式经行类的实现。将声明统一放在 Date.h 中,把成员函数的定义统一放在 Date.cpp 中,道理跟C语言的声明定义分离一样。
#include<iostream>#include<cassert>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);// 检查日期是否合法bool CheckDate();// 全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1);// 拷贝构造函数 // d2(d1)Date(const Date& d);// 赋值运算符重载 // d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d);// 析构函数~Date();// 日期+=天数Date& operator+=(int day);// 日期+天数Date operator+(int day);// 日期-天数Date operator-(int day);// 日期-=天数Date& operator-=(int day);// 前置++Date& operator++();// 后置++Date operator++(int);// 后置--Date operator--(int);// 前置--Date& operator--();// >运算符重载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);// 日期-日期 返回天数int operator-(const Date& d);void Print() {cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};// 输出流重载ostream& operator<<(ostream& out, const Date& d);// 输入流重载istream& operator>>(istream& in, Date& d);
这份地图中大家也许会发现很多陌生的内容,如友元,重载前置++和后置++,流插入和流提取重载等。不过不用着急,接下来都会讲到。
获取某年某月的天数:GetMonthDay
GetMonthDay函数用于获取某年某月的天数,由于其在日期加天数和减天数运算符重载的函数中被频繁调用,且代码量较少,我们可以直接将其设置为内联,定义到类的内部(定义到类内部的成员函数都默认加了内联inline,而分离定义的成员函数没有此特性)。
注:分离声明和定义的函数是无法内联的。
int GetMonthDay(int year, int month){assert(month > 0 && month < 13);int months[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 months[month];}
由于此函数定义在类的内部,故没有 Date:: 来指定命名空间,此函数的其他逻辑应该也好理解,最终通过传过来的年和月来确定当月的天数,考虑到闰年判断等问题。
检查日期合法,构造函数,拷贝构造函数,赋值运算符重载及析构函数
检查日期是否合法,需要检查月不能小于1或者大于12,日要根据年和月来判断,见代码:
bool Date::CheckDate() // 由于声明和定义分离,定义函数时需指定一下命名空间Date::{if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)) {return false;}else return true;}
构造函数,拷贝构造以及赋值重载等没什么好说的,注意拷贝构造需判断一下日期是否合法:
// 全缺省的构造函数Date::Date(int year, int month, int day){_year = year;_month = month;_day = day;if (!CheckDate()) {cout << "日期非法\n" << endl;}}// 拷贝构造函数// d2(d1)Date::Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}// 赋值运算符重载 // d2 = d3 -> d2.operator=(&d2, d3)Date& Date::operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;return *this;}// 析构函数Date::~Date(){_year = 0;_month = 0;_day = 0;}
日期类的+=day和+day
日期类可以+吗?答案是肯定的,不过不是两个日期相加,而是一个日期去+一个天数,如,今天是2024/4/20,加上5就是五天后,五天后是2024/4/25。如何实现+=,来看看代码:
// 日期+=天数Date& Date::operator+=(int day){ // 此处是对+的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;}
如果day为大于0的数,首先将day的值加到成员变量_day上,然后循环判断_day是否大于当月天数,如果大于,就减去当月天数,_month+1,_year根据_month是否大于12判断自增;如果_day小于当月天数,则跳出循环。最后返回值是为了贴合内置类型连续运算赋值的规则,如,a=b+=c;而Date&引用返回则是为了减少拷贝。
写道这里,就即将到类和对象一个神奇的使用方式了,它的名字叫——复用!
我们可以直接复用+=来实现+,来看看具体的复用代码:
// 日期+天数Date Date::operator+(int day){Date tmp(*this);tmp += day; // 复用+=return tmp;}
日期+day的规则是不能改变运算日期的值,所以这里在成员函数中定义了一个tmp拷贝,用来执行运算操作,最后传值返回,是因为tmp作为临时变量,出了函数就会销毁。
注:这里提供的是+=复用+的设计模式。实际上,用+复用+=也是可行的,但是这种复用方式却比+=复用的质量差,大家感兴趣可以自行实现并考虑其中原因。
日期类的-=day和-day
-=day虽然思路和+=差不多,但是逻辑却比+=难搞一些,建议仔细思考后在上手写。
// 日期-=天数Date& Date::operator-=(int day){ // 当减的天数为负时的操作if (day < 0) {*this += (-day);// 复用了刚才写的+=return *this;}_day -= day;while (_day < 1) {--_month;if (_month == 0) {--_year;_month = 12;}day += GetMonthDay(_year, _month);}return *this;}
在给_day+=天数时,注意加的是上个月的天数,并非本月天数。写了-=,-的实现也就是简单的复用操作了:
// 日期-天数Date Date::operator-(int day){Date tmp(*this);tmp -= day;return tmp;}
返回值是否引用返回和+=day和+day是同理的。
前置++和后置++
知道内置类型的前置++和后置++吗?如++a,a++等,它们的作用都是给变量a自增1,学了这么长时间,你是否对它们的区别了如指掌。本篇我们讲的是自定义类型的自增,如,日期类,我们要给一个自定义类型(日期类)自增,但计算机并不知道日期自增的规则,这时就需要人为提供内置类型的自增运算符。其实,上面刚刚讲过复用,我们直接复用+=和-=就可以了,你想想,什么是++?什么是--?不就是+=1和-=1嘛!
// 前置++:返回+1之后的结果Date& Date::operator++(){*this += 1;return *this;}
注:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率。
如果不提供参数,在C++的定义之下,默认就是放到对象前面的重载运算符:++d,但如果单将一个运算符放到对象后面构成运算符重载应该怎么办呢?看看C++提供的解决方式:
// 后置++:自增,返回自增之前的拷贝Date Date::operator++(int){Date tmp(*this);*this += 1;return tmp;}
前置++和后置++都是一元运算符,为了可以让前置++和后置++形成正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用参数时该参数不用传递,编译器自动传递。
注:后置++是先使用后+1,因此只需要返回+1前的值,故需要先将this保存一份,然后给this+1。而代码中的tmp是临时对象,因此只能以值得方式返回,而不能返回引用。
前置--和后置--
不想多说了,四个字——同理,复用!
// 前置--Date& Date::operator--(){*this -= 1;return *this;}// 后置--Date Date::operator--(int){Date tmp(*this);*this -= 1;return tmp;}
注意区分前置和后置的区别,控制下返回值是否引用返回就行。
比大小运算符的重载
为什么要给日期提供比大小重载,因为内置类型无法完成比较日期的任务。比大小需要提供的重载函数就多了,包括 <,<=,==,>,>=,!= ,一套写下来,岂不是能把人累瘫,别着急,实际上,需要你完成的实际上只有两个,> 和 ==,当然你选择 < 和 ==,或者 <,!= 统统都可以,如果还没想通,没关系,来看看我是如何实现的,这里以> 和 ==为例。
// >运算符重载bool Date::operator>(const Date& d){if (_year > d._year|| _year == d._year && _month > d._month|| _year == d._year && _month == d._month && _day > d._day) return true;return false;}// ==运算符重载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 || *this == d;}
发现C++运算符重载和复用的魅力了吗?
如果此时你需要一个 < 重载:
// <运算符重载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);}
如果……哦,已经没有如果了,这就是所有的比大小运算符重载?,很有意思吧。
日期-日期返回天数
日期减日期为的是计算两个日期之间的天数间隔。对于日期相减,这里提供了两种实现方案。
暴力++法
找出日期中小的那个,然后通过记录每次++的次数n来记录两日期之间的间隔,同时设置一个变量flag来记录两个日期之间的差是正的还是负的。
// 日期-日期 返回天数int Date::operator-(const Date& d){Date max = *this;// 假设大的那个是*thisDate min = d;int flag = 1;int n = 0;if (*this < d) { //判断并重置max和min以及日期间隔正负max = d;min = *this; flag = -1;}while (min != max) {++min;++n;}return n * flag;}
此方法也是最好理解的一种,虽然暴力++会有一些消耗,但是对于目前计算机的算力来说却是微不足道的。
直接相减法
这个过程就稍稍有些复杂,你可以先确定max(大日期对象)下一年的第一天为tmpmax和min(小日期对象)本年的第一天tmpmin,让它们的差值✖365,加到 n 上,同时遍历一遍这些年,找到一个闰年就让n+1。最后定义两个整型变量(tmp1,tmp2),让tmpmin逐次++(++使用的是之前重载的日期类++,最好用前置++,减少拷贝的消耗),同时用tmp1计数,直到和min相等;让max逐次++,同时tmp2计数,直到和tmpmax相等。这时让n减去tmp1和tmp2后,得到的就是两个日期之间的差值了。
以下是实现代码,亲测正确:
int Date::operator-(const Date& d){Date max = *this;Date min = d;int flag = 1;int n = 0;if (*this < d) {max = d;min = *this;flag = -1;} // 以上是比较日期确定max和minn += (max._year - min._year + 1) * 365;for (int i = min._year; i <= max._year; i++)if (i % 4 == 0 && i % 100 != 0 || i % 400 == 0)++n; // 以上是根据年计算间隔的天数Date tmpmax(max._year + 1, 1, 1);int tmp1 = 0;Date tmpmin(min._year, 1, 1);int tmp2 = 0;while (tmpmax != max) {++max;++tmp1;}while (tmpmin != min) {++tmpmin;++tmp2;} // 以上是日期矫正,计算tmp1和tmp2n -= tmp1;n -= tmp2;return n * flag;}
对于日期相减,不止有这两种实现,大家想到一些别的方式也可以自己亲自试一试,还是很有意思的。
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
具体怎么修饰呢?写一个const在参数列表后面:
在()括号后放const是将隐式传递的*this指针类型变成了const *this指针类型,这种解决方案是为了应对*this无法被显示改变为const类型而产生的。
对于const类型的对象,和内置类型的const修饰规则非常相似,权限只能平移和缩小,而不能放大。看看代码案例:
const Date d1;Date& d2 = d1;// 不支持,d2是Date类型,取d1的引用属权限放大const Date& d3 = d1;//支持,权限平移Date d4;const Date& d5 = d4;// 支持,权限缩小
故对于const成员函数内部,是不可以调用非const成员函数的;而非const成员函数却可以调用其他的const成员函数。
所以,对于一些不会改动Date对象数据的成员函数,尽量提供const类型的成员函数即可,如下:
// 获取某年某月的天数const版int GetMonthDay(int year, int month)const{assert(month > 0 && month < 13);int months[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 months[month];}
GetMonthDay定义在类内部,故不需要限定命名空间。
// 检查日期是否合法const版bool Date::CheckDate()const{if (_month < 1 || _month>12|| _day < 1 || _day>GetMonthDay(_year, _month)) {return false;}else return true;}
基本上就是在函数()后加上const即可,需要这样调整的函数还包括比大小重载函数,日期相减函数,-day和+day,Print打印日期函数等等,这里就不一一列举了。
流插入和流提取重载
大家之前可能接触过C语言,这里就能体现出C语言输入输出的局限性了,它没办法支持对象类型的输入输出。在C++标准库<iostream>(包含istream类和ostream类)中,内含了输入流对象cin和输出流对象cout,通过使用这两个对象,我们可以支持输出所有的内置类型变量。今天要讲的流插入和流提取重载,就是可以帮助大家直接用cin和cout支持内置类型的输入输出。
如果需要定义一个流插入的重载,你会如何定义呢?
void Date::operator<<(ostream& out){ cout << _year << "-" << _month << "-" <<_day <<endl;}
是这样吗?那就大错特错了,按照运算符重载规则,
cout << d;
这种书写方式明显不符合重载时的参数顺序,所以如果想要调用上面这份重载,需要这样写:
d << cout;
但是,这种使用方式明显违背了像内置类型那样使用输入输出流的初衷。产生这种问题的主要原因还是无法改变调用类内部定义的成员函数时,第一个传过去的元素永远是this指针。
为解决这样的问题,需要我们把流插入和流提取的函数重载在类的外部,像下面这样:
ostream& operator<<(ostream& out, const Date& d){out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;}
这里返回一个对象引用类型是为了贴合内置类型的使用规则,连续输出,如cout << a << b << c;如果没有此返回值,那么使用对象时,就只能用一次cout打印一个Date对象,如cout << d;而不是cout << d1 << d2 << d3;。
流提取也是同样的方法:
istream& operator>>(istream& in, Date& d){cout << "请输入年月日,用空格分隔:";in >> d._year >> d._month >> d._day;if (!d.CheckDate()) {cout << "日期非法\n" << endl;}return in;}
返回一个对象的引用也是为了贴合内置类型使用规则。
到这里,你不免会问,如果将重载函数定义在了类的外部,那么该如何使用类内部定义的私有成员变量,如_year,_month,_day呢?别急,接下来补一下之前挖的坑,友元。
友元(friend)
在C++中,友元(Friend)是一个特殊的机制,它允许一个非成员函数或者一个类(或类的成员函数)访问另一个类的私有(private)或保护(protected)成员。友元不是类的成员,但它可以访问类的所有成员,包括私有和保护成员。这种访问权限的赋予是通过在类的定义中使用friend
关键字来实现的。只需要在类的内部添加上类外定义的函数的声明,并在声明前加上关键字friend即可,一般这种友元函数允许写在类内部的任意地方,一般来说会把它放在整个类的开头。当一个函数成为一个类的友元,那么这个函数内部就可以随意使用类中的私有(private)或保护(protected)成员了。
class Date{// 友元friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);public: //。。。private: //。。。}
结语
本篇博客实现了日期类Date,加深对运算符重载的运用,讲了前置++和后置++:传一个改变成后置++样式的int;还讲到const成员:由于无法改变*this类型而添加的const;流插入和流提取重载:由于无法改变传参顺序而定义到类的外部等等。这篇博客也算是对之前学到类和对象内容的一个阶段性的应用和总结吧。
博主后续还会产出更多有意思的内容,感谢大家的支持!♥