在声明和定义分开中,作用域限定符的使用
构造函数如何初始化对象
析构函数要不要自定义呢?
不用,因为内置类型自动处理,自定义类型需要
在标准实现功能时,==::==的意义,不要忘记
要注意运算符重载时触发的方式,是类的对象之间直接计算,而不是按正常的类型计算
搞清楚诸如+ ,+=符号在运算的意义,以及对操作对象的影响,不要搞混了
+= 和+在逻辑上的区别
+在实行结束后,是不改变本身的值,而是把+之后的结果作为返回值,故要用要临时变量,只能传值返回。+=最后会改变左操作数的结果,
1.前言
在我们学习完类和对象后,针对类和对象繁杂的知识,我们可以尝试写一个完整的类,来加深了解,今天笔者就介绍一个简单的日期类。在这个日期类里,实现日期比较,日期加减等一些基本功能。
对于我们常用的日期,一般我们只会查看多少天以后是什么日子,或者几个月几年后是什么日子。对于一般的几个月几年后的计算,我们自己简单就能计算出来,而平时生活中我们基本不会去直接算两个日期相加减的情况,因此,在日期类的实现中我们以天数为主。
2.初始化
对于类的初始化,构造函数是必不可少的,但是像我们以往简单的写一个构造函数是不行的。对于日期类,在对象初始化时有可能值是非法的,如下所示:
Date d1(2022,2,29);
Date d2(2022,3,32);
Date d3(2022,13,1);
以上几种情况,是在初始化时有可能出现的,即天数非法,月份非法,而在天数中,又有闰年的特殊情况。以下我们简单说说:
日期非法,由于每个月天数都大不相同,因此判断日期非法的第一步就是获取每月的天数。我们可以写几个if语句,或者用一个switch语句,这里笔者介绍一个比较好用的方法。我们可以创建一个数组,把每个月的天数放进去,对应月份就可以简单的获取天数了。
//整个日期类的实现都是将函数声明和定义分开的,严格点地实现可以帮助我们更好掌握类和对象的知识。要注意在类外实现时不要忘了作用域限定符
int Date:: GetMonthDay(int year, int month)
{
int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int tmp = days[month];
if (month == 2 &&(year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
tmp++;
}
return tmp;
}
Date::Date(int year, int month, int day)
{
if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
{
this->_year = year;
this->_month = month;
this->_day = day;
}
else
{
cout << "日期非法" << endl;
}
}
//简单说说判断闰年的原理:因为地球公转,每年的时间是365天多一点点,每4年刚好多出一天,每100年刚好不多,而每400年又刚好多出一天,因此被4整除但不能被100整除。
实现完构造函数,我们接着实现拷贝构造函数,以及打印函数,方便我们查看日期,这就我们好说的,大家可以参考以下代码:
//拷贝构造
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//打印天数
void Date::show()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
针对后面我们要实现的几个赋值运算符重载,我们想想在已经实现了拷贝构造后opeartor=要不要实现呢?
答案是要的。拷贝构造仅仅是一个拷贝,它在我们诸多函数运行中都会调用,比如传值返回的时候,产生某些临时变量的时候。而opeartot=,我们想想,不仅仅是d1 = d2,也有可能是d1=d2=d3=d4这样的连续赋值,因此在实现opeartor=时,赋值的结果还要作为返回值传递,这是二者的区别。当然,我们可以实现拷贝构造,在operato=中调用拷贝构造,最后将值返回。
//=实现
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
最后我们再思考一个问题——析构函数要不要实现呢?
在过往的博客中,笔者说过,对于内置类型,编译器会自动回收,自定义类型才会调用对应的析构函数,而日期类的实现又没有涉及到自定义类型,因此不必实现。
3.赋值运算符重载
3.1 +与 +=
我们之前学习运算符重载时说过,重载不能改变运算符本身的含义,这在我们实现+和+=中很好地体现了。我们暂不论实现思路,我们想想+和+=的区别是什么?
d1 = d2 + d3;
d1 += 1;
//在第一个表达式中,我们观察可以发现,对于+,它不会改变两个操作数的值,而是将d2+d3整个表达式的值作为返回值。
//在第二个表达式中,+=直接将表达式计算的值赋给了左操作数d1,因此我们可以得+与+=的区别:+不直接赋值,是把整个计算的结果作为返回值传递,而+=会直接赋值。
在实现之前,我们想想+和+=尽管意义不同,但都要实现加这个过程,那我们还有没有必要去两次都去完整实现呢?答案是不用的,我们完全可以实现一个,另一个再去复用,这样就可以减少代码量了。比如我们实现+=,实现+的时候再调用+=就好了。当然具体实现+也行,只不过由于+要使用临时变量,因此笔者个人认为具体实现+=会方便点,接下来我们就可以尝试实现了,具体如下:
//+=实现
Date& Date:: operator+=(int day)
{
//如果+=的有右操作数是负数,我们可以直接把它换成-=
if (day < 0)
{
*this -= day;
return *this;
}
//在实现加法时,我们可以把天数全部加上去,然后使用进位的方法,满月进1,满年进1.
_day = _day + day;
while(_day > GetMonthDay(_year, _month))
{
//注意_month进位的时机
_day = _day - GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
return *this;
}
//+实现
//由于+是将计算的结果作为返回值传递,所以我们要利用临时变量计算返回值,再把结果返回,由于是临时变量,故只能传值返回,不能传引用。
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
在+=的实现中,细心的朋友可能发现一个循环中我们连续调用了两个GetMonthDay,既然调用的都是同一个月份的,那能不能优化呢?
如果我们在循环外定义临时变量x,假设当前月是3月,此时x = 31,下次再进去循环,x的值还是不变,因此不能使用这种方法。for循环呢?也不太合适,尽管for循环的初始化可以定义临时变量,但是每个月的天数需要我们动态获取,但for循环只初始化一次,这样看来调用两次也是一个无奈的选择了,各位有什么好的办法也可以尝试优化一下。
3.2 思维误区
针对刚开始实现的重载,我们要明白一件事:我们重载操作符,为的是后续可以像内置类型一样正常使用各种操作符,而我们在实现时,要实现的是对象中内置类型数据的各种赋值操作,两者是有区别的,千万不要搞混了。
int x = 1;
int y = 0;
Date d1(2023,2,2);
x = x + y;//正常使用
d1 = d1 + 5;//我们要是实现的就是对象可以这样正常使用操作符,这样才是正常的调用
d1._day = _day + d1._day;//这是我们在内部实现时要做的
3.3-与-=重载
实现-与-=,思路上其实和+与+=是差不多的,只是在具体细节上有点差别,便不再多说了,具体如下:
//-=实现
Date& Date:: operator-=(int day)
{
//和+=一样,减负数就调用加法
if (day < 0)
{
*this += day;
return *this;
}
_day = _day - day;
while (_day <= 0)
{
//这里_month退位的时机和+=不一样,大家自己感受下
_month--;
int x = GetMonthDay(_year, _month);
_day =_day + x;
if (_month == 0)
{
_year--;
_month = 12;
}
}
return *this;
}
//-实现
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
3.4日期-
这里单独把日期-拿出来,因为前面的-与-=实现的是对象直接和天数加减,日期-要实现的两个日期相互加减,尽管我们说两个日期相减不太常用,但是可以的话我们还是来实现一下,同时学习一下,像日期类的数据,怎么实现相减,思路如下:
(1)日期相减,按常规思路,我们可以直接相减,得出还剩几年几个月几天,但是我们思考一下,实现日期类的意义是什么?
我们如果想知道两个日期之间有几年,有几个月,我们自己就可以计算出,这样我们就可以大致推算出,日期-的用途,即知道两个日期间到底有几天。
这样看来,单纯两个日期-,如果直接相减,那么为了获得天数,我们还要计算天数,由于我们此时得到的日期并不是固定的,而天数进位也是不固定的,因此在后续获得天数上就非常麻烦了,这种方法我们就先不考虑了。
(2)有朋友可能想到,既然两个日期直接相减不方便,那我借一个日期来做比较是不是可以。这确实是个好思路,拿1971.1.1来说(这一天刚好是星期一,由此计算更加方便),我们把减法转换成加法,分别计算出和1971.1.1的天数差距,进而得出两个日期之间的天数差距。
这不失为一个好办法,那我们再想想这个过程可不可以优化?可不可以不借助中间变量呢?实际上,当我们思想扭转过来,把减法装换成加法,就很简单了。我们完全可以让二者计数,选出较小值,让较小值计数逼进较大值,从而获得相差的天数。代码如下:
//日期-
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
//注意这种获得大小值的思路,默认最大值最小值,比较后根据结果再调整最值。其实这种思路和比较后再决定最值差不多,但是方便得是在逻辑上一开始我们定下了最值,把参数换成了最值,方便我们思考
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while(min != max)
{
n++;
min++;
}
return n * flag;
}
(3)尽管我们淘汰了临时值直接计数计算差值,获取了天数,但是我们只会单纯计算天数吗,更多的我们会想知道到底是星期几,农历几号,公历几号。由此,结合1971.1.1这个特殊的日期,我们可以通过与它的减法来计算出星期几,以满足我们实际的需求,代码如下:
//借助我们实现-和-=的思路来实现,对求余不太清楚的朋友,可以自己画个图感受一下
void Date::GetWeekDay()
{
Date tmp(1971, 1, 1);
int x = *this - tmp;
const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
cout << arr[x % 7] << endl;
}
3.5 前置++ 和后置++
在类和对象赋值运算符重载中我们介绍了前置++和后置++ 的区别,这里就不在多说了,不太明白的朋友可以看看之前的博客:
源码如下,大家可以借鉴一下:
//前置++
Date& Date::operator++()
{
_day += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
3.6opeartor<< 和operator>>
既然我们实现了日期类,针对C++的流输入和流提取,我们能不能对==<<==和==>>==进行重载,让它们支持直接打印日期呢?
首先我们看看cout的资料:
我们可以发现,cout是在ostream这个类内的,而C++cout支持自动识别类型,也是靠函数重载完成的,因此我们只需要根据上面的函数声明,照猫画虎写一个就行(同样的,流输入cin也是这样的,只不过它属于istream)。但是由于我们在使用cout时需要遵守一定的格式,故我们最好在类外声明和定义,这样可以保证参数与输出格式一致。代码如下,大家可以参考一下:
ostream& operator<<(ostream& _cout, Date& d)
{
cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
void Test2()
{
Date d1(2023, 2, 22);
cin >> d1;
cout << d1 << endl;//endl表示一行输入结束,然后输出下一行,cin才是流输入
}
int main()
{
Test2();
return 0;
}
//调用看一看
4比较运算符重载
其实完成运算符重载,日期类就基本完备了,但是我们完整一点,同时让大家更了解函数复用,我们再实现一下==,>, < , >=, <=, !=这几个运算符。相信大家再完成+,+=等赋值运算符重载后,这几个运算符对大家都不算太难,我们主要想想我们需不需要再去实现每一个呢?
其实不用,我们可以只实现两个,比如>和==,剩下的运算符都可以用这两个函数表示,我们看看:
//>实现
bool Date::operator>(const Date& d)
{
if ((_year > d._year)
|| (_year == d._year && _month > d._month)
|| (_month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
//==实现
bool Date::operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
// >=运算符重载
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);
}
以上几个运算符重载实现大家具体感受一下,最后把源码展示出来给大家参考一下:
//Date.h
#pragma once
#include<iostream>
using namespace std;
//流输入,流提取
//ostream& operator<<(ostream& _cout, Date& d);
//获取某年某月的天数
class Date
{
public:
friend ostream& operator<<(ostream& _cout, Date& d);
friend istream& operator>>(istream& _cin, Date& d);
//获取天数
int GetMonthDay(int year, int month);
//全缺省的构造函数
/*Date(int year = 1, int month = 1, int day = 1)
{
if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
{
this->_year = year;
this->_month = month;
this->_day = day;
}
else
{
cout << "日期非法" << endl;
}
}*/
Date(int year = 1, int month = 1, int day = 1);
//拷贝构造
Date(const Date& d);
//析构函数
/*~Date();*/
/*~Date();*/
//赋值运算符重载
Date& operator=(const Date& d);
//展示函数
void show();
//日期+=天数
Date& operator+=(int day);
//日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
//前置++
Date& operator++();
//后置++
Date operator++(int);
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
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);
void GetWeekDay();
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#include"Date.h"
//构造函数,这样合适吗,对于我们的初始化,万一非法了怎么办?应该要判断,对于闰年怎么判断
Date::Date(int year, int month, int day)
{
if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
{
this->_year = year;
this->_month = month;
this->_day = day;
}
else
{
cout << "日期非法" << endl;
}
}
//获取天数,判断闰年,讲讲闰年的大概原理,原理是每年多出一点点世界,4年刚好多出一天,而100又抵消了,400年又多出一天,因此4年闰,百年不闰,400年闰
int Date:: GetMonthDay(int year, int month)
{
int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int tmp = days[month];
if (month == 2 &&(year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
tmp++;
}
return tmp;
}
//拷贝构造
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//打印天数
void Date::show()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//=实现
Date& Date::operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
//+=实现
Date& Date:: operator+=(int day)
{
if (day < 0)
{
*this -= day;
return *this;
}
_day = _day + day;
while(_day > GetMonthDay(_year, _month))
{
_day = _day - GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
return *this;
}
//+实现
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
//-=实现
Date& Date:: operator-=(int day)
{
if (day < 0)
{
*this += day;
return *this;
}
_day = _day - day;
while (_day <= 0)
{
_month--;
int x = GetMonthDay(_year, _month);
_day =_day + x;
if (_month == 0)
{
_year--;
_month = 12;
}
}
return *this;
}
//-实现
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
//前置++
Date& Date::operator++()
{
_day += 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)
|| (_month == d._month && _day > d._day))
{
return true;
}
else
{
return false;
}
}
//==实现
bool Date::operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
{
return true;
}
else
{
return false;
}
}
//日期-
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while(min != max)
{
n++;
min++;
}
return n * flag;
}
//计算星期几
void Date::GetWeekDay()
{
Date tmp(1971, 1, 1);
int x = *this - tmp;
const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
cout << arr[x % 7] << endl;
}
//流输入流提取
ostream& operator<<(ostream& _cout, Date& d)
{
cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
istream& operator>>( istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}