通过类和对象的了解我们可以知道,类是一种自定义类型。今天我们就要创建一个可以保存某一天的年、月、日三个信息的类。此外,这个类还可以完成一些相对基础的操作,例如计算两个日期之间相差多少天、比较两个日期的大小等。
一个类最基础的就是六个默认成员函数,他们分别是构造函数,析构函数,拷贝构造,赋值重载,以及两个不常用的取地址及const对象取地址。由于后两个不常用,下面的实现中不会进行说明。
1.构造函数与析构函数
一个类最基础的功能就是初始化与清理,所以我们先讲讲这两个函数的实现。
首先是构造函数,构造函数的作用是完成一个对象的初始化。
构造函数的函数名与类名相同,没有返回值,参数不限。我们按照这个规定实现构造函数。
我们知道,我们写了构造函数的话,编译器就不会生成构造函数。所以这个构造函数最好是全缺省的,以保证没有实参传入时也能正常进行初始化。
而对于日期类,其成员变量有三:年 月 日。
private://限定下面的成员为私有
int _year;//为了方便与成员函数中的形参区分,所有的成员变量都加上_
int _month;
int _day;
很显然,构造函数的参数也应该是年月日。
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
我们以1900年1月1日作为缺省日期,就得到了上面的构造函数。
但是,这样的方式是否有所不妥?
很显然,按照上面的逻辑,我们输入任意的三个数字,都可以成为一个日期。但是现实中的日期有着严格的规定,所以我们应该对不合法的日期进行判断并提示。
这里我们实现一个成员函数GetMonthDay()用以判断当前月份有多少天:
int GetMonthDay()
{
int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//利用一个数组保存每个月的天数,arr[0]为0 以方便月份能与下标一一对应
if (_month == 2 && ((_year % 4 == 0 && _year % 100 != 0) || _year % 400 == 0))//特例:如果是闰年的二月,则该月天数为29
{
return 29;
}
else//否则这个月的天数就是数组中对应月份的天数
{
return arr[_month];
}
}
这样,我们就可以改写我们的构造函数了:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if ((_month < 1 || _month>12) || (_day<1 || _day>GetMonthDay()))//判断日、月是否合法
{
cout << "日期非法" << endl;
}
}
接下来是析构函数,析构函数的作用是在对象生命周期结束时将类所占用的资源释放。
析构函数没有返回值,没有参数,函数名是类名前加一个~。自动生成的默认析构函数对于内置类型不会处理,而自定义类型则会调用它们的析构函数。
但是日期类明显没有什么需要我们来释放的资源,所以日期类的析构函数可以不写,而是直接使用默认析构函数即可。
2.拷贝构造
拷贝构造的作用是用一个已经存在的对象来拷贝出一个新的对象。
拷贝构造没有返回值,函数名和类名相同,参数为同类的对象的引用。
而默认拷贝构造会进行浅拷贝,也就是直接将存在对象进行内存拷贝,达到值拷贝的效果。但是这种浅拷贝会导致新的对象的指针指向存在的对象的空间,而在进行操作时,两者会共同使用一个空间。这显然违反了拷贝的初衷。并且在析构时会对一块空间进行释放,导致程序出错。
所以在需要进行深拷贝的情况下,我们就不能直接使用默认拷贝构造了。但是日期类并不需要进行深拷贝,所以我们只进行简单的值拷贝即可。
另外说明,拷贝构造形参必须为引用。原因如下:
我们知道,当函数传参时,若使用传值传参而不是传引用,那么会在传参时对实参进行拷贝形成形参。而这个拷贝需要调用拷贝构造,而调用拷贝构造又会引发传参,所以程序会在传参—拷贝实参之间陷入死循环。而传引用并不涉及拷贝构造,所以这里必须使用传引用。
Date(const Date& d)//必须传引用
{
_year = d._year;//将存在对象的值拷贝给新的对象
_month = d._month;
_day = d._day;
}
3.赋值重载与运算符重载
1.什么是运算符重载
可以见到,C++提供了许多的运算符,例如> < ++ --等等,他们可以帮助我们对一个数值进行运算、比较等操作。但是,日期类能不能直接进行比较呢?显然我们的电脑还没有那么智能,所以运算符重载就给了我们这个机会。通过运算符重载函数,我们可以将不同的运算符与自定义类型、基本类型相结合,产生新的意义。
Date addDay(Date& d,int day);
假设上方的函数内部功能为返回日期加一个天数后得到的日期。可以见得,调用这个函数时需要写成
addDay(d,10);
这样的可读性并不好,而上面的函数若写成运算符重载函数并作为日期类的成员函数,函数的声明可以写成
Date operator+(int day);
其中operator+为函数名,表示该函数重载了+。而+的左操作数是隐含的this指针,右操作数是day,返回Date类型。在调用的时候,我们可以使用以下方式调用该函数:
d+10;
这样,与直接使用加法无异,可读性大大提高。这也就是为什么要使用运算符重载了。
2.赋值重载的实现
赋值重载函数的参数为同类的对象的引用,返回值类型为该类。函数名为operator=。
对于日期类的赋值重载,我们只需要进行年月日的拷贝即可。
Date& operator=(const Date& d)
{
if (this != &d)//自己给自己赋值时直接返回自身
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
3.其他运算符的重载
其他运算符较多,我们以代码+注释的形式进行解释说明
// 日期+=天数
Date& operator+=(int day)
{
if (day > 0)当右操作数为正数
{
_day += day;//直接将右操作数加到天数上
while (!(_day > 0 && _day <= GetMonthDay()))//当日期不合法,进行年、月的修正
{
_day -= GetMonthDay();//日期减少当前月份的总天数
_month++;//月份增加
if (_month == 13)//12月再增加后循环为1月,年份增加
{
_month = 1;
_year++;
}
}
}
else if (day < 0)//当右操作数为负数,交给-=处理
{
*this -= -day;
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date copy(*this);//+之后不改变对象本身,故使用拷贝对象进行+的操作
return copy += day;//复用+=,返回拷贝对象+=后的值
}
Date operator-(int day)//与+同理
{
Date copy(*this);
return copy -= day;
}
Date& operator-=(int day)//与+=同理
{
if (day > 0)
{
_day -= day;
while (_day<1 || _day>GetMonthDay())
{
_month--;
if (!_month)
{
_month = 12;
_year--;
}
_day += GetMonthDay();
}
}
else if (day < 0)
{
*this += -day;
}
return *this;
}
// 前置++
Date& operator++()
{
return *this += 1;//对象自增 然后返回自增后的对象
}
// 后置++
Date operator++(int)//后置++的对象要先进行运算,后自增
{
*this += 1;//对象自增,达到第二步的效果
return *this-1;//返回对象-1,达到返回的是自增前的日期
}
// 后置--
Date operator--(int)//后置的第二种方法
{
Date copy(*this);//拷贝一个对象,用来返回自减前的值
*this -= 1;//对象自减
return copy;//返回自减前的值
}
// 前置--
Date& operator--()//同前置++
{
return *this -= 1;
}
// >运算符重载
bool operator>(const Date& d)//大于有三种情况:年份大于、年份相同,月份大于、年份月份都相同,日期大于,所以可以直接对着三种情况进行或运算判断是否大于然后直接返回结果
{
return (_year > d._year) || ((_year == d._year&&_month > d._month)) || (_year == d._year&&_month == d._month&&_day > d._day);
}
// ==运算符重载
bool operator==(const Date& d)//年月日都等于才相等
{
return _year == d._year&&_month == d._month&&_day == d._day;
}
// >=运算符重载
inline bool operator >= (const Date& d)//复用>和==
{
return *this>d||*this==d;
}
// <运算符重载
bool operator < (const Date& d)//同上,复用
{
return !(*this >= d);
}
// <=运算符重载
bool operator <= (const Date& d)//同上,复用
{
return !(*this > d);
}
// !=运算符重载
bool operator != (const Date& d)//同上,复用
{
return !(*this == d);
}
// 日期-日期 返回天数
int operator-(const Date& d)//对一个日期++或--,直到两者相等,记录++或--的次数
{
int ret = 0;
Date copy(*this);//使用拷贝对象以避免修改原对象
while (copy != d)
{
copy > d ? (copy--, ret++) : (copy++, ret--);
}
return ret;
}
还有两个特殊的重载:<<和>>流插入和流提取。由于使用着两个运算符时,对象是作为右操作数,而成员函数则由于自带this指针,使得对象永远是第一个操作数。所以我们通过友元函数的形式,完成这两个函数的重载。
class Date
{
public:
friend ostream& operator<<(ostream&, const Date&);//在类的内部声明这两个函数是友元函数
friend istream& operator>>(istream&, Date&);
//...
//...
};
ostream& operator<<(ostream& cout, const Date& d)//返回cout以达到连续输出
{
cout << d._year << '-' << d._month << '-' << d._day;//以年-月-日的格式输出
return cout;
}
istream& operator>>(istream& cin, Date& d)
{
cin >> d._year >> d._month >> d._day;
return cin;
}