目录
1、前言
本篇文章我们将主要实现以下的这些接口:
#include <iostream>
using namespace std;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) const;
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
//打印接口
void Print() const;
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
//总结一下:只读函数可以加const,内部不涉及修改成员的都是只读函数
// >运算符重载
bool operator>(const Date& d) const;
// ==运算符重载
bool operator==(const Date& d) const;
// >=运算符重载
bool operator>=(const Date& d) const;
// <运算符重载
bool operator<(const Date& d) const;
// <=运算符重载
bool operator<=(const Date& d) const;
// !=运算符重载
bool operator!=(const Date& d) const;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-天数
Date operator-(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 函数重载
// 运算符重载
// 前置++
Date& operator++(); //++d1 -> d1.operator()
// 加一个int参数,进行占位,跟前置++构成函数重载进行区分
// 后置++
Date operator++(int); //d1++ -> d1.operator(0)
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// 日期-日期 返回天数
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
本次的项目我们以多文件来写,包含以下三个文件:
接下来我们就对以上的接口一一进行实现:
2、全缺省的构造函数
对于全缺省的构造函数正常写的时候存在一个不足,万一传参传的月份与天是不存在的,虽然对实例化的对象初始化了,但是是违法的,因此我们需要判断一下,这里就需要我们对月份的天数写一个函数,构造的时候先对比一下。
我们这里先实现GetMonthDay接口:
// 获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
static int monthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if(2 == month
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return monthDay[month];
}
// 全缺省的构造函数
Date::Date(int year, int month, int day)
{
if (month < 1 || month > 12
|| day < 1 || day > GetMonthDay(year, month))// 判断日期是否合法
{
cout << "非法日期" << endl;
exit(-1);
}
else
{
_year = year;
_month = month;
_day = day;
}
}
对于GetMonthDay接口,我们写了一个数组存每个月有多少天,默认二月是28天,在下面我们会判断一下如果是找二月的天数,对年进行判断,看看是否为闰年,为闰年的时候直接返回29,其他的就直接返回月份对应的天数。我们对数组使用static修饰,因为后面会不断的调用此函数,因此我们将其放到静态区,只开辟一次,节省了时间,一次的时间不多,但是如果是大量的调用会极大的提升效率。
这里我们会有人问,为什么在GetMonthDay接口后面加一个const,这里我们来说明一下:
3、打印接口
打印接口没什么讲的,我们直接一把梭哈:
//打印接口
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
打印接口对this使用const修饰是因为,有可能我们传过来的对象是const修饰的,如果Print接口不加const就涉及权限放大的问题,导致出错。改为const后就不存在权限问题了。
4、拷贝构造
对于拷贝如果还有不清楚的可以点击后面的链接,里面有对拷贝构造详细讲解哦:点这里哦
// 拷贝构造函数
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
5、赋值运算符重载(operator=)
5.1赋值重载是默认成员函数,重载格式:
参数类型:const T&,传引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this:要复合连续赋值的含义
我们按重载格式来写一下:
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
if (this != &d)// 存在this就是d的情况
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
5.2 赋值重载不能为全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
我们自己写一下试试:
我这里使用的vs2019编译器,编译器会提示,编译期间直接报错了,因此赋值运算符重载是不可以为全局函数的。
5.3 编译器默认生成
当我们不写的时候编译器会自动生成一个复制重载函数,但是默认生成的函数对内置类型是直接赋值的,对自定义类型的成员变量需要调用对应的类赋值重载函数来完成赋值。
因此,如果成员变量里存在自定义类型(类类型),自定义类型的赋值重载函数必须是正确的,这样才是对的。
如两个栈实现一个队列,队列的赋值重载函数可以默认生成,但是栈的必须自己写,因为栈存在申请资源,如果直接拷贝,两个栈使用的是同一块资源,这样的话,一个是出栈的栈,一个是进栈的栈,不管进出其实是对同一个栈进行操作,这就是错误的。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
赋值与拷贝的区别:拷贝是一个已经存在的对象去初始化另一个要创建的对象;
赋值是两个已经存在的对象进行拷贝。
6、析构函数
对于析构函数如果还有不清楚的可以点击后面的链接,里面有对析构函数详细讲解哦:点这里哦
// 析构函数
Date::~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
7、operator>
对 日期>日期 的判断中
1、当年小于后,就不用再比了,直接返回false
2、当年相同比较月,月小于后,直接返回false
3、当年月都相同,日小于后,返回false
4、当以上判断都不正确,说明前面的日期 大于 后面的日期,返回true
// >运算符重载
bool Date::operator>(const Date& d) const
{
if (_year < d._year)
return false;
else if (_year == d._year && _month < d._month)
return false;
else if (_year == d._year && _month == d._month && _day < d._day)
return false;
else
return true;
}
8、operator==
对 日期==日期 的判断很简单,年月日分别都相等就是正确的,我们看看代码实现:
// ==运算符重载
bool Date::operator==(const Date& d) const
{
if (_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;
}
9、operator>=
对于 >= 来说,我们其实可以复用上面的 >与== ,不用再去写一套逻辑来判断,>= 的逻辑就是大于或者等于。
我们来看看实现代码:
// >=运算符重载
bool Date::operator>=(const Date& d) const
{
return (*this > d) || (*this == d);
}
10、operator<
< 与 >= 是相反的逻辑,因此我们对 >= 取反就可以实现。
// <运算符重载
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
11、operator<=
<= 与 > 是相反的逻辑,因此我们对 > 取反就可以实现。
// <=运算符重载
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
12、operator!=
!= 与 == 是相反的逻辑,因此我们对 == 取反就可以实现。
// !=运算符重载
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
13、operator+= (日期+=天数)
+=是在原基础上进行修改,因此隐含的this不能用const修饰,因为在原基础上修改,所以出了+=函数体,对象还在,我们使用引用返回,这样可以减少拷贝。
我们画图对 += 天数分析:
代码实现:
// 日期+=天数
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
//月进位
_month++;
//月满了
if (13 == _month)
{
_year++;
_month = 1;
}
}
return *this;
}
我们来测试一下:
对照看看我们写的+=是否正确
14、operator+ (日期+天数)
+与+= 的区别在于+=是对对象本身+,而+不是对对象本身的改变。所以我们需要实例化一个新的对象,将传来的对象拷贝给新的对象,存放+天数的结果并返回。
// 日期+天数
Date Date::operator+(int day) const
{
Date tmp(*this);
tmp += day;
return tmp;
}
这里我们拷贝了一份之后,就能复用+=的代码。
我们这里看看传的对象与+后的对象的结果:
这里我们可以看到+并没有影响到d1的结果。
15、operator-= (日期-=天数)
-=是在原基础上进行修改,因此隐含的this不能用const修饰,因为在原基础上修改,所以出了-=函数体,对象还在,我们使用引用返回,这样可以减少拷贝。
我们画图对-=天数进行分析:
代码实现:
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
_month--;
if (0 == _month)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
运行结果:
对比一下,看我们实现的是否正确:
16、operator- (日期-天数)
-与+ 的逻辑是类似的,-与-= 的区别在于-=是对对象本身-,而-不是对对象本身的改变。所以我们需要实例化一个新的对象,将传来的对象拷贝给新的对象,存放-天数的结果并返回。
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
这里拷贝一份this之后,就可以在拷贝的tmp上复用-=。
我们来测试一下:
17、前置++,后置++,前置--,后置--
对于前置++与后置++,前置--与后置--,它们的函数名是相同的,但是实现的功能是不同的,如果在符号表里找这怎么能分得清楚呢?
对于这样的问题,C++有自己确定的规定,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。后置--也是如此。
这样就能实现函数重载,符号表中函数名虽然相同,但是一个有参数一个没参数。
下面我们对这几个函数接口分别实现:
前置++的规则是:先++,再使用。因此前置++就相当于+=1,复用+=。
// 前置++
Date& Date::operator++() //++d1 -> d1.operator()
{
*this += 1;
return *this;
}
后置++的规则是:先使用,再++。这里我们先将this拷贝一份,然后对this+=1,返回拷贝的对象,这样就做到了先使用,再++。
// 后置++
Date Date::operator++(int) //d1++ -> d1.operator(0)
{
Date tmp(*this);
*this += 1;
return tmp;
}
前置--的规则是:先--,再使用。因此前置++就相当于-=1,复用-=。
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
后置--的规则是:先使用,再--。这里我们先将this拷贝一份,然后对this-=1,返回拷贝的对象,这样就做到了先使用,再--。
// 后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
18、日期 - 日期(返回天数)
此接口是 日期-日期,这里有一个存在一个小细节,当 小日期-大日期 的时候,返回值就是负数,因此我们对这个细节要注意一点,我们来对这个接口的思路梳理一遍:
1、我们用假设法,假设this是大日期,将其赋值给max对象,第二个参数是小日期,将其赋值给min对象,并定义一个flag初始化为 1;
2、比较一下,如果this是小日期那么将max、min两个对象的内容重新赋值,并将flag赋值为 -1;
3、定义一个计数器 n,让小追大,++min一次,计数器也++,追上后就跳出循环,返回 n*flag 即可。
注意:我们让小追大的时候使用前置++,虽然前置++与后置++都可以实现,但是后置++的接口中需要拷贝两次,开始拷贝一次,返回值会再拷贝一次,前置++在大量计算的时候可以实现时间与空间双重优势。
我们来实现代码:
// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
int flag = 1;
Date max = *this;
Date min = d;
if (max < min)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
测试:
19、const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
这里我们使用 Print 接口来讲:
使用const修饰后,与原本的成员函数是不冲突的,因为是构成重载的。
我们来看看两种版本的 Print 接口:
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
对于打印函数来讲,函数内部是不涉及修改成员的,这种就是只读函数。
因此我们使用了const修饰,还存在一种问题,当我们的对象是const修饰,如果Print函数不用const修饰,在调用的时候就存在权限放大问题。
我们先将const修饰的Print函数频闭掉:
我们再将const修饰的接口放开看看:
总结:相同函数,const修饰的this与不修饰的函数构成重载;
函数内部是不涉及修改成员的就是只读函数,const修饰后更安全,并且对于具有常属性的对象也可以兼容。