文章目录
一、前言
在我们的日常生活中,我们可能需要计算几天后的日期,或计算日期差等,现如今计算日期的方式有很多,简单粗暴的直接查看日历,快捷点的直接使用日期计算器来求得,先给一个网络上的日期计算器截图:
现在,就让我们用代码来实现其工作原理吧。
- 注意:本篇日期类**.h文件放声明,.cpp**文件放定义
二、日期类的实现
1.Date类中默认成员函数的使用
1.构造函数
//构造函数 - 可写可不写
Date::Date(int year, int month, int day)
{
if (year >= 1 && month <= 12 && month >= 1 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
cout << "日期非法" << endl;
}
日期类的构造函数需要对日期的合法性进行判断。
2.析构函数
//析构函数 - 可写可不写
Date::~Date()
{
;
}
日期类并没有申请资源(动态开辟内存,打开文件),所以这里我们写不写都可以,系统会默认生成。
3.拷贝构造函数
//拷贝构造函数 - 可写可不写
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
系统默认生成的拷贝构造函数会对内置类型进行浅拷贝,所以我们也不用写,但是如果有有资源的对象时,需要深拷贝。
4.赋值运算符重载
//赋值运算符重载 - 可写可不写
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
也可不写,使用系统默认生成的即可。拷贝构造和赋值运算符重载的区别在于拷贝构造用于对象构造时使用,而赋值运算符重载用于已存在对象赋值时使用。后续处理有资源的对象时,需要先把旧空间释放,再开一块同样大小的空间,进行数据拷贝。
5.const成员函数
//其中的例子
//日期 - 天数
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
下面有很多函数都用到了const修饰,这是因为成员函数默认第一个参数为 Date* const this,而const Date* 指向的内容不能被修改,可是当它传给Date* 时就出错了,因为Date* 是可以修改的,这里传过去会导致权限放大。加上const去保护this指向的内容,也就是在函数的后面加上const。
6.取地址操作符重载和const取地址操作符重载
//取地址操作符重载
Date* Date::operator&()
{
return this;
//return nullptr;
}
//const取地址操作符重载
const Date* Date::operator&()const
{
return this;
//return nullptr;
}
不用自己写,除非想让别人通过取地址操作符获取到特定值(自己在重载函数内部写)或屏蔽类地址。
2.检查日期的合法性
实现日期类首先就得检查日期的合法性,这其中就包括大小月,闰年的2月有29天,一年只有12个月等等细节都要考虑到。
class Date { public: bool isLeapYear(int year) //判断是否为闰年 { //四年一闰百年不闰或四百年一闰 return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } int GetMonthDay(int year, int month) { //加上static防止函数频繁调用开辟几十个字节大小的数组,最好加上 static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2 && isLeapYear(year)) return 29; //闰年2月29天 else return monthDayArray[month]; } Date(int year = 1, int month = 1, int day = 1) { //这里我们不考虑公元前 if (year >= 1 && month <= 12 && month >= 1 && day <= GetMonthDay(year, month)) { //确保日期合法 _year = year; _month = month; _day = day; } } private: int _year; int _month; int _day; };
- 因为GetMonthDay这个函数需要在日期类中被频繁调用,所以将 monthArr存放至静态区,减少数组频繁开辟、销毁空间的开销。
- 这里由于闰年比较特殊,需要单独拿出来处理。并且这里我们不考虑公元前的日期。
3.运算符重载
< 运算符重载、== 运算符重载
- 思路:
< 运算符重载在我上一篇博文已经详细讲解过,主要是先把大于的情况全部统计出来,就比如我要比较实例化对象d1是否小于实例化对象d2,只需考虑如下三种满足的情况:
- d1的年小于d2的年
- d1与d2年相等,d1的月小于d2的月
- d1与d2年相等月相等,d1的天小于d2的天
这三种全是小于的情况,返回true,其余返回false
- 代码如下:
// <运算符重载 bool Date::operator<(const Date& d) const //类外访问成员函数需要设定类域 { if (_year < d._year || _year == d._year && _month < d._month || _year == d._year && _month == d._month && _day < d._day) return true; else return false; }
- 思路:
== 运算符重载其实非常简单,只需要判断d1和d2的年、月、天是否对应相等即可:
- 代码如下:
// ==运算符重载 bool Date::operator==(const Date& d) const { return _year == d._year && _month == d._month && _day == d._day; }// ==运算符重载
<= 运算符重载、> 运算符重载
- 思路: 复用
<= 的运算符重载,这里要仔细想一想 <= 成立的条件是啥。不就是 要么 < 要么 = 吗?我们只需要复用先前写的 < 运算符重载和 <=运算符重载,无需自己耗费精力写。
- 代码如下:
// <=运算符重载 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); }
- 思路: 复用
有了前面的基础,写个 != 也很简单,对 == 取反即可
- 代码如下:
//!=运算符重载 bool Date::operator!=(const Date& d) const { return !(*this == d); }
4.改进和优化
上述我们写的运算符重载都是建立在声明定义分离的,这里我们可以对其进行优化,如下:
先前我们学过内联,可以帮助我们对于短小函数减少函数调用而引发的效率损失问题,因此我们可以把上述几个运算符重载函数放成内联,此外,有一种简单粗暴的方法:直接在类里定义,因为类里的函数默认内联,还省的我们自己写inline,而且我们也不用在类外加上类域了,当然,有些长的函数还是声明和定义分离比较好。
- Date.h 文件:
#pragma once #include<iostream> #include<assert.h> using namespace std::cout; using namespace std::cin; using namespace std::endl; class Date { public: bool isLeapYear(int year) { //四年一润百年不润或四百年一润 return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } //获取某月天数 int GetMonthDay(int year, int month); //构造函数 Date(int year = 1, int month = 1, int day = 1); //打印 void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } // <运算符重载 bool operator<(const Date& d) const; // ==运算符重载 bool operator==(const Date& d) const; // <=运算符重载 bool operator<=(const Date& d) const { return *this < d || *this == d; } // >运算符重载 bool operator>(const Date& d) const { return !(*this <= d); //return (d < *this); } // >=运算符重载 bool operator>=(const Date& d) const { return !(*this < d); } // !=运算符重载 bool operator!=(const Date& d) const { return !(*this == d); } private: int _year; int _month; int _day; };
- Date.cpp 文件:
#include"Date.h" //获取某月天数 int Date::GetMonthDay(int year, int month) { assert(year >= 0 && month > 0 && month < 13); static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2 && isLeapYear(year)) return 29; else return monthDayArray[month]; } //构造函数 Date::Date(int year, int month, int day) { if (year >= 1 && month <= 12 && month >= 1 && day <= GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else cout << "日期非法" << endl; } // <运算符重载 bool Date::operator<(const Date& d) const //类外访问成员函数需要设定类域 { if (_year < d._year || _year == d._year && _month < d._month || _year == d._year && _month == d._month && _day < d._day) return true; else return false; } // ==运算符重载 bool Date::operator==(const Date& d) const { return _year == d._year && _month == d._month && _day == d._day; }
5.日期操作
日期 + 天数、日期 += 天数
- 思路:
对于日期 + 天数,我们得到的还是一个日期。特别需要注意进位的问题(天满了往月进,月满了往年进),主要考虑如下几个特殊点:
- 加过的天数超过该月的最大天数,需要进位
- 当月进位到13时,年进位+1,月置为1
- 法一:
Date Date::operator+(int day) const { Date ret(*this); //拷贝构造,拿d1去初始化ret _day += day; while (_day > GetMonthDay(ret._year, ret._month)) { _day -= GetMonthDay(ret._year, ret._month); _month++; if (ret._month > 12) { _year++; _month = 1; } } return ret; }
出了作用域,对象ret不在,它是一个局部对象,我们这里不能用引用,用了的话,返回的就是ret的别名,但是ret又已经销毁了,访问野指针了,所以出了作用域,如果对象不在了,就不能用引用返回,要用传值返回
- 法二:复用日期+=天数
此法是建立在日期+=天数的基础上完成的,这里各位可以先看下文日期+=天数,然后我们进行复用:
Date Date::operator+(int day) const { //法二:复用日期 += 天数 Date ret(*this); ret += day; return ret; }
- 法一和法二熟优?
答案:法二更好,也就是用+去复用+=,具体原因在下文会解释。
这里实现 += 其实有两种方案
- 法一:
前面我实现的日期+天数,仔细观察我的代码,函数的第一行,我就调用了一个拷贝构造:
Date ret(*this); //拷贝构造,拿d1去初始化ret
这里调用拷贝构造,是为了不在* this本身上做变动,只在ret上进行操作,其理由是日期+天数得到的是另一个日期,而不用拷贝构造直接在*this上做改动只会导致原有的日期也变化,而这个变化正是我日期 += 天数的需求。
仔细想想:+=天数就是在原有的日期上再加一定的天数,直接对*this做手脚即可,因此只需对日期+天数的代码进行小改动即可:
Date& Date::operator+=(int day) //传引用返回 { //如果day小于0,要单独处理 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; }
注意这里是传引用返回,原因就在于我返回的*this是全局的,出了作用域还在
- 法二:复用日期 +天数
Date& Date::operator+=(int day) //传引用返回 { //如果day小于0,要单独处理 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只要>0就没问题,若小于0就要借位了。要注意当减去的天数<0时单独讨论。具体步骤如下:
- 当减的天数为负数,则为+=,直接调用
- 若减后的day<0,月-1
- 若月 = 0,则年-1,月置为12
- 代码如下:
//日期 -=天数 d1-=100 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; }
前置 ++、后置 ++
- 思路:
C++里有前置++和后置++,这就导致一个巨大的问题,该如何区分它们,具体实现过程不难(直接复用+=即可),难的是如何区分前置和后置。因此C++规定,无参的为前置,有参的为后置。
- 代码如下:
//前置++ Date& Date::operator++() //无参的为前置 { *this += 1; //直接复用+= return *this; }
- 思路:
有参的即为后置,后置++拿到的返回值应该是自己本身未加过的,因此要先把自己保存起来,再++*this,随后返回自己。
- 代码如下:
//后置++ Date Date::operator++(int i) //有参数的为后置 { Date tmp(*this); *this += 1; //复用+= return tmp; }
前置 --、后置 –
- 思路:
前置–和前置++没啥区别,只不过内部复用的是-=
- 代码如下:
//前置-- Date& Date::operator--() //无参的为前置 { *this -= 1; //直接复用-= return *this; }
- 思路:
后置–和后置++类似,只不过内部复用的是-=,不再赘述
- 代码如下:
//后置-- Date Date::operator--(int i) //有参数的为后置 { Date tmp(*this); *this -= 1; return tmp; }
日期 - 日期
- 思路:
日期 - 日期得到的是天数,首先我们得判断两个日期的大小,用min和max代替小的和大的,随后,算出min和max之间的差距,若min!=max,则min就++,随即定义变量n也自增++,最后返回n(注意符号)
- 代码如下:
//日期 - 日期 int Date::operator-(const Date& d) const { int flag = 1; //方便后续计算正负 Date max = *this; Date min = d; if (*this < d) { min = *this; max = d; flag = -1; //计算正负 } //确保max是大的,min是小的 int n = 0; while (min != max) { min++; n++; }//算出min和max之间绝对值差距 return n * flag; //如果d1大,结果为正,d2大结果为负 }
6.<<流插入、>>流提取运算符的使用
- 思路:
这里我们重载<<操作符,运算符重载是有要求的,这里的d1必须是左操作数,那么cout一定是右操作数。那么我们只能使用如下方式调用。
void Date::operator<<(ostream& out) { cout << _year << "年" << _month << "月" << _day << "日" << endl; } int main() { Date d1, d2; d1 << cout; // d1.operator << (cout); }
我们写成成员函数,就只能这样写,因为隐藏的this指针默认抢了第一个参数位置。日期类对象就是左操作数,不符合使用习惯和可读性。
那么我们采取其他的办法,既然成员函数就只能这么写,那我们把它变成全局函数,写在类外面,这时候我们就可以自己控制参数的顺序。
//operator(cout, d1) cout << d1; void operator<<(ostream& out, const Date& d) { cout << d._year << "年" << d._month << "月" << d._day << "日" << endl; }
但是这里又会有一个大问题,我们不能访问私有的成员变量?那我们把成员变量改成公有试一试?
这里其实会出现报错。这是因为这个函数是全局函数,在Date.cpp、Test.cpp预处理的时候会被展开,造成多重定义的冲突问题,只要是全局函数/全局变量,都会出现这样的问题。那我们怎么解决?
可能大家都知道应该声明与定义分离。
这里我们还可以用另一个方法,把它变成静态成员函数。
为什么这样就不报错呢?我们回忆一下static的作用,能够改变变量/函数的生命周期。但是它还有另一个作用,会改变变量/函数的链接属性:只在当前文件可见,就不用进符号表,告知编译器我当前文件用就可以了。
那我们这里还有没有什么问题呢?我们赋值的时候有时候会链式调用,这时候就会出问题。
因为 cout << d1先结合,但是并没有返回值,这里应该有一个cout的返回值。这里我们更改一下。
这里我们其实还可以再优化一下,直接把它变成内联函数,不仅解决频繁调用效率低的问题,还能解决冲突问题。
因为内联函数在用的时候直接展开,也不用进符号表。
但是我不想让成员变成公有,能不能改变呢?
这里同样有两种方式:
1.创建一个公有的成员函数getYear,getMonth,getday,去访问私有成员变量,java很喜欢用这种方式。
2.调用友元,允许你访问私有的成员变量。
三、总结
本篇日期类把先前学到的引用,传值/传引用返回、拷贝构造、复用等等知识点柔和到了一起,非常值得大家操手练习练习,创作不易,还望三连。
- 日期类的源码链接:日期计算器