文章目录
前言
之前介绍了C++面向对象的初阶知识,为了更理解相关知识,加深印象,我们可以先试着写个日期类来巩固所学。
1.日期类的功能分析
1.大致分析
我们写日期类之前可以先去百度看看一些简蛋的日期转化工具的大致功能,从中提炼出我们自己日期类的功能。我大致将日期类分为以下功能:天数转化日期(给定一日期往前推算日期或往后推算日期),两个日期减相差多少天,为了用到运算符重载的知识可以进行日期的判断比较,已经针对输入输出实现输入输出的运算符重载。
2.接口设计
日期类构造函数这里就不算在内了,同时日期类也不设计资源申请,这里就不提析构函数的实现了,这里主要是围绕日期类功能的相关接口。
打印函数 void Print();
得到月份的天数 int GetMonthDay(int year, int month);
日期+=天数 Date& operator+=(int day);
日期+天数 Date operator+(int day);
日期-天数 Date operator-(int day);
日期-=天数 Date& operator-=(int day);
后置++ Date operator++(int);
后置-- Date operator--(int);
前置++ Date& operator++();
前置-- Date& operator--();
赋值重载 Date& 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);
!=运算符重载 bool operator != (const Date& d);
日期-日期 返回天数 int operator-(const Date& d);
流插入 friend ostream& operator<< (ostream& out, const Date& d);
流提取friend istream& operator>>(istream& in, Date& d);
日期类的成员这都很好确定,大致就3个:year month day.
2.具体实现
1.日期类的成员函数和成员变量
class Date
{
public:
//打印函数
void Print();
//得到月份的天数
int GetMonthDay(int year, int month);
//全缺省构造函数
Date(int year=1, int month=1, int day=1);
//拷贝构造
Date(const Date &d);
// 析构函数
~Date()
{
;
}
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
//前置++
Date& operator++();
// 前置--
Date& operator--();
//赋值重载
Date& 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);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
//流插入
friend ostream& operator<< (ostream& out, const Date& d);
//流提取
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
其实这个日期类成员全凭借个人意愿,有些功能如果没想好,可以边写边想,或者后序加上也行。不过最好还是写代码之前提前想好,这样就可以专心写代码实现功能了。
2.初始化(构造函数)
之前我们知道构造函数就是用来初始化的,这里我就实现了两个构造,一个直接构造,另一个是拷贝构造。这里要注意一点对于用户输入的日期要进行判断,不能使用非法的值用来构造。
其实这里的拷贝构造我们不现实也可,可以使用默认生成的,因为日期类不涉及一些资源的申请。
Date::Date(int year=1,int month=1,int day=1)
{
if(month > 0 && month < 13&&
(day > 0 && day<=GetMonthDay(year, month)))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法初始化" << endl;
}
}
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
我们可以先写个Getmonthday函数用来获取每一年每个月的天数,这里要注意对闰年的判断。
int Date:: GetMonthDay(int year, int month)
{
int MonthDay[13] =
{ 0,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 MonthDay[month];
}
}
这里只用对2月进行单独处理即可。当然也可以加入一些对输入年份和月份判断限制。我这里就没有那么严谨进行限制判断。
3.对日期进行天数推算
这里就需要用到了运算符重载的知识了,天数推算就是日期类对象进行天数的相加或者相减。为了支持这种相减相加的操作,只能对+ -运算符进行重载了。
关于日期天数加键的逻辑分析
+= 与-=
Date& Date::operator+=(int day)
{ //复用-=运算符重载
if (day < 0)
{
*this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date& Date:: operator-=(int day)
{ //复用+=运算符重载
if (day < 0)
{
*this +=-day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
这里使用引用返回是为了更加符合运算符特性,比如:b=(a+=c),如果没有返回值的话,出现例子种的场景显然是有问题的。代码中还涉及复用的技巧,对于为负值的天数相加时,可以复用相减的运算符重载,就是减去负day。处理相减时也可以这样处理,如果day是负数,就是加上负day。
+ 与-
Date Date::operator+(int day)
{ //复用+=
//tem是临时变量所以不用引用返回
Date tem = *this;
tem += day;
return tem;
}
Date Date::operator-(int day)
{
Date tem = *this;
tem += day;
return tem;
}
这里要提一点 a+=1 a的值肯定是改变了, a+1 这个a的值肯定是没有改变的。基于此实现- +运算符重载的时候,先创建一个临时变量,为了不该变原来对象中的数据。因为是在函数体内创建的临时变量实用这里就使用的传值返回。当然这个返回值的也是为了符合运算符的特性:比如a=b-1
因为之前实现了+=和-=这里对临时变量直接复用即可。
这里补充一个运算符重载=赋值运算符重载
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
这里实现的时候多加了一个判断自己对自己赋值的时候不用处理,减少不必要的程序系统开销。
为了支持运算符连续赋值的特性,这里采用的也是引用返回。
4.比较相关的运算符重载
之前我们实现- +运算符重载的时候使用了代码复用,这里也可以代码复用,我们可以先实现两个运算符重载,其他的都可以复用,我选取的<和==,其他都是复用这两个。
bool Date::operator==(const Date& d)
{
return _year == d._year &&
_month == d._month && _day == d._day;
}
bool Date::operator<(const Date& d)
{
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;
}
else
{
return false;
}
}
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 || *this == d;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
当我们使用了<和==之后,如果两个日期类对象,是小于或者等于就实现了<=,如果不是<=就是大于,如果大于或者等于就实现了>=,如果不是 == 就是!=,这样一来代码的复用就实现了。代码复用算是一个编程小技巧,尽可能的多用。
5.前置后置自增或自减
前置++ 我们知道改变自己,后置++不改变自己。同时为了保持原本运算符的连续性,运算符重载是需要有返回值的,这点上面就提到过,返回值类型根据运算符特性特性来决定,如果是改变自己采用引用返回即可,其他情况就采用采用传值返回。
前置++ 代码示例
Date& Date::operator++()
{
//复用
*this += 1;
return *this;
}
后置++ 代码示例
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
tmp+= 1;
return tmp;
}
这里要提一下,C++中为了区分前置++和后置++ ,后置++多了一个int 参数 主要是为了构成函数重载,用于区分这两个运算符重载,实际使用的时候不用传参。同理,后置--的时候也需要这样处理。
前置-- 代码示例
//前置--
Date& Date:: operator--()
{
*this -= 1;
return *this;
}
后置-- 代码示例
//后置--
Date Date::operator--(int)
{
Date tem(*this);
tem -= 1;
return tem;
}
当然这里实现的时候也是代码复用
6.日期相减与流插入流提取
1.日期相减
日期相减我们可以引入一个计数变量用于计算两个日期的差值,我们先选出较小的日期进行前置++直到和较大的日期相等为止,每自增一次就计数一次。这样就可以很好的计算出日期差值。
代码示例
int Date::operator-(const Date& d)
{
Date max(*this);
Date min(d);
int flag = 1;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
//复用!=
int day = 0;
while (max != min)
{
++min;
++day;
}
return flag * day;
}
可能前者的日期比后者的大,所以引入一个变量flag标记一下,用于确定结果的正负值。这里还是涉及到代码的复用。
2.重载流插入和流提取
我们刚接触c++的时候可能觉得cout和cin很神奇能够自动识别数据类型,
通过之前的学习我们不难猜出cont和cin是使用了运算符重载和函数重载能够自动识别数据类型。我们可以去c++官网查一下cout和cin.
我们不难看出cin 是iostrem头文件中的一个对象,它的类型是istream,cout也是iostrem头文件中的一个对象,它的类型是ostream。
因为c++的官网库中对内置类型进行函数重载和运算符重载了,但是没有对日期类进行重载,我们日期类无法直接cout << Date 或者cin>> Date ,我们不能动官方库,只能直接对是>>进行重载,如果将其封装成Date类的成员函数就会发生如下问题:
这样写用倒是能用 但是过于奇怪了。为了更加和库中的使用方法保持一致,
我们将这个函数写成全局函数,写成全局函数后就有个问题不能访问类中的成员,为了能够访问类中成员我们必须得使用friend将这个全局函数设置成类中的友元函数来处理。
同理,cin也是这样处理。
//为了能沟连续输出或输入采用引用作为返回值
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;
}
3.总结
日期类实现难点在于理清加减天数的逻辑,这点是比较重要的。理清后后面的代码就很好写了。为了得到每个月的天数这个功能是频繁使用的,我们可以将其封装成一个函数,这样代码逻辑更加清晰。
编程技巧:
学会代码的复用,这样可以使得代码更加简洁,同时对于一些不用改变成员变量的函数形参可以使用const修饰,尽可能使用const是个好习惯。比如上述的日期比较函数不仅可以使用const修饰还可以让其成为const成员函数,专门用于修饰*this。因为这种只需要读权限即可,const 对象也可以调用该成员函数。
以上内容如有问题,欢迎指正,谢谢!