文章目录
- 引言
- 一、为什么需要运算符重载?
- 二、日期类的实现
- 1. 基本框架
- 2. 预备工作
- 3. Date 类中六大默认成员函数的使用
- 3.1 全缺省的构造函数
- 3.2 拷贝构造函数
- 3.3 析构函数
- 3.4 赋值运算符重载
- 3.5 const 成员函数
- 3.6 取地址操作符重载和const取地址操作符重载
- 4. 运算符重载
- 4.1 += 和 + 运算符重载
- 4.2 -= 和 - 运算符重载
- 4.3 前置++ 和 后置++ 运算符重载
- 4.4 前置-- 和 后置-- 运算符重载
- 4.5 > 和 == 运算符重载
- 4.6 >= 、< 、<= 和 != 运算符重载
- 4.7 日期-日期
- 4.8 流插入 << 和 流提取 >> 运算符重载
- 5. 日期类源码
- 5.1 Date.h文件
- 5.2 Date.cpp 文件
- 总结
引言
在C++编程中,运算符重载是一种强大的功能,它允许我们为自定义的数据类型定义运算符的行为。这种特性在创建像日期(Date)这样的类时尤其有用,因为它允许我们更自然、更直观地操作这些类的实例。通过日期类我们还能够实现一个简单的日期时间计数器的功能,想必大家都很期待接下来的内容。
在本篇博客中,我们将深入探索如何为Date类重载运算符,并了解其中的一些最佳实践和潜在陷阱。
注:Date.h
文件下放日期类的声明代码,Date.c
文件下放日期类的实现代码
一、为什么需要运算符重载?
运算符重载可以让我们使用更直观、更自然的方式来操作日期。
在创建日期类时,我们可能希望执行如下操作:
- 对两个日期进行加法运算以得到一个新的日期(例如,将5天加到今天的日期上)。
- 比较两个日期以确定哪个日期更早或更晚。
- 从一个日期中减去另一个日期以得到它们之间的时间差。
- 等等…
为了实现这些操作,我们需要为Date类重载相应的运算符,如+、-、<等。
二、日期类的实现
1. 基本框架
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) const;
// 检查日期是否合法
bool CheckInvalid();
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// ======== 有关日期计算的运算符重载 ========
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// =========== 比较运算符的重载 ==========
// >运算符重载
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;
// 日期-日期 返回天数
int operator-(const Date& d);
// 打印,const成员函数修饰的是成员函数隐含的this指针,表明不能修改任何成员变量
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 友元声明,可以在类的任何位置
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
// 类外声明
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
2. 预备工作
在构造日期类时我们很有可能不小心传入错误的日期时间,比如 2023-02-29 或 2024-4-31这样的时间,所以为了确保日期的正确性,在构造函数中我们需要检查日期的合法性,因此在写构造函数前需要先实现两个成员函数:
CheckInvalid()
函数:功能是检查日期是否合法GetMonthDay(int year, int month)
函数:功能是获取某年某月的天数
// 获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
static int days[13] = { 0,31,28,31,30,31,30,31,30,31,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return days[month];
}
// 检查日期是否合法
bool Date::CheckInvalid()
{
if (_year <= 0
|| _month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
return true;
}
注意:这里 days
数组被声明为静态数组,静态数组在程序的生命周期内只会被初始化一次。由于 GetMonthDay
函数被频繁调用,将其声明为静态可以确保它只在程序开始时分配一次内存,而不是每次调用函数时都重新分配。这可以提高空间效率。
3. Date 类中六大默认成员函数的使用
3.1 全缺省的构造函数
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!CheckInvalid())
{
cout << *this << "构造日期非法" << endl;
exit(-1);
}
}
通过构造函数即可对实例化对象进行初始化。
3.2 拷贝构造函数
拷贝构造:同类型的一个已经存在的对象去初始化一个新的要创建的对象
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
3.3 析构函数
// 析构函数
~Date()
{
//cout << "~Date()" << endl;
}
日期类并 没有申请资源(动态开辟内存,打开文件等)
,所以这里的析构函数可写可不写,系统默认生成的就够用。
3.4 赋值运算符重载
赋值重载:已经存在的两个对象,一个拷贝赋值给另一个
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
3.5 const 成员函数
// 举例:
// 打印,const成员函数修饰的是成员函数隐含的this指针,表明不能修改任何成员变量
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针
,表明在该成员函数中 不能对类的任何成员进行修改
。
如果我们只需要访问成员变量而非修改成员变量,那么此时我们就可以使用 const成员函数
来提高代码的健壮性。
3.6 取地址操作符重载和const取地址操作符重载
// 取地址操作符重载
Date* operator&()
{
return this;
//return nullptr;
}
// const取地址操作符重载
const Date* operator&()const
{
return this;
//return nullptr;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
这里以注释的两个return语句为例,可以返回空指针给别人,让对方找不到指定的类对象。(所以大多数情况下我们都不需要重载这两个成员函数,使用编译器默认生成的即可)
4. 运算符重载
此处详细讲解 += 和 + 运算符的重载,-= 和 - 运算符的重载与其类似。
因此,对于前置++和后置++,前置- -和后置- -运算符重载,详细讲解前者
4.1 += 和 + 运算符重载
注:实现了 +=
运算符的重载后,对于 +
运算符的重载就可以利用 +=
运算符来实现。
// 日期+=天数
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
if (_month == 12)
{
_month = 1;
_year++;
}
else
{
_month++;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day)
{
Date temp = *this; // 调用拷贝构造,因为temp不是一个已存在的对象
temp += day;
return temp;
}
-
operator+=
的算法思想和步骤:- 输入:
Date
对象的一个引用和一个整数(代表天数)。 - 算法思想:将给定的天数加到当前日期的天数上,并相应地调整月份和年份,以确保日期仍然有效。
- 步骤:
- 将当前日期的天数(
_day
)与给定的天数相加。 - 检查更新后的天数是否超过了当前月份的天数(通过调用
GetMonthDay
函数)。 - 如果超过了,从当前月份的天数中减去超出的部分,并更新月份和年份。
- 如果月份是12月,将月份设置为1月,年份加1。
- 否则,将月份加1。
- 返回
Date
对象的引用,以便进行链式操作。
- 将当前日期的天数(
- 输入:
-
operator+
的算法思想和步骤:- 输入:
Date
对象的一个实例和一个整数(代表天数)。 - 算法思想:创建一个
Date
对象的副本,并将给定的天数加到副本上,然后返回调整后的日期副本。 - 步骤:
- 创建一个
Date
类型的临时对象temp
,并通过拷贝构造函数将其初始化为当前Date
对象的副本。 - 使用
operator+=
函数将给定的天数加到temp
对象上。 - 返回调整后的
temp
对象(值拷贝返回)。
- 创建一个
- 输入:
注:返回Date
对象的引用(在operator+=
中)允许进行链式操作,如 dateObj += 5 += 3
。
4.2 -= 和 - 运算符重载
// 日期-天数
Date Date::operator-(int day)
{
Date temp = *this;
temp -= day;
return temp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
while (day >= _day)
{
day -= _day;
if (_month == 1)
{
_month = 12;
_year--;
_day = GetMonthDay(_year, _month);
}
else
{
_month--;
_day = GetMonthDay(_year, _month);
}
}
_day -= day;
return *this;
}
4.3 前置++ 和 后置++ 运算符重载
-
前置++:返回+1之后的结果
- 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
-
后置++:前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确的重载,C++规定:后置++重载时多增加一个
int
类型的参数,但调用函数时该参数不用传递,编译器自动传递- 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将 this 保存一份,然后给 this + 1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date temp = *this;
*this += 1;
return temp;
}
4.4 前置-- 和 后置-- 运算符重载
// 后置--
Date Date::operator--(int)
{
Date temp = *this;
*this -= 1;
return temp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
4.5 > 和 == 运算符重载
注意:对于比较运算的重载,只要先重载了 > 和 == 运算符,其他运算符的重载就可以利用这两者来实现。
// >运算符重载
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
{
if (_day > d._day)
return true;
}
}
return false;
}
// ==运算符重载
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
4.6 >= 、< 、<= 和 != 运算符重载
// >=运算符重载
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.7 日期-日期
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
int flag = 1;
Date maxDate = *this;
Date minDate = d;
if (*this < d)
{
flag = -1;
maxDate = d;
minDate = *this;
}
int n = 0;
while (maxDate != minDate)
{
minDate++;
n++;
}
return flag * n;
}
-
思路
- 比较日期:首先比较当前对象(
*this
)和传入的日期对象d
,确定哪个日期更大,哪个更小。 - 确定符号:根据日期的大小关系确定结果的符号。如果当前对象更大,则结果为正;如果传入的日期更大,则结果为负。
- 计算天数差:
从较小的日期开始,逐步增加一天,直到两个日期相等。每次增加一天,就增加计数器的值
。 - 返回结果:根据符号和天数差计算最终的结果,并返回。
- 比较日期:首先比较当前对象(
-
步骤
- 声明变量:
flag
:用于表示结果的符号,初始化为1(表示正数)。maxDate
和minDate
:用于存储两个日期中较大和较小的日期,初始时分别设置为当前对象(*this
)和传入的日期d
。
- 比较日期并确定符号:
- 使用
if
语句比较*this
和d
。 - 如果
*this
小于d
,则交换maxDate
和minDate
的值,并将flag
设置为-1。
- 使用
- 初始化计数器:
- 声明
n
并初始化为0,用于计算天数差。
- 声明
- 计算天数差:
- 使用
while
循环,每次循环将minDate
增加一天(即调用minDate++
),并增加计数器n
的值。 - 循环继续直到
maxDate
和minDate
相等。
- 使用
- 返回结果:
- 将
flag
与n
相乘,得到最终的结果,并返回。
- 将
- 声明变量:
4.8 流插入 << 和 流提取 >> 运算符重载
有了日期类那么我们需要打印日期信息该怎么办呢?
- 我们可以实现一个 Print 成员函数来实现:
// 打印,const成员函数修饰的是成员函数隐含的this指针,表明不能修改任何成员变量
void Print() const
{
cout << _year << "-" << _month << "-" << _day;
}
但是每次都要调用函数来打印日期是不是太麻烦了,有没有更简单的方法,就像内置类型(int、char等)那样直接使用 cout 打印呢?
- 在C++中,对于自定义类型可以重载流插入 << 来实现像内置类型一样的输出操作。
cout 是一个 ostream 类型的对象。
// 流插入<< 重载
ostream& operator<<(ostream& out, const Date& d)
{
// 输出格式化字符串到输出流
out << d._year << "-" << d._month << "-" << d._day << endl;
// 返回输出流引用以支持链式操作
return out;
}
【思路】:
- 定义重载函数:定义一个函数,接受一个
ostream
引用和一个const Date&
引用作为参数。 - 输出格式化字符串:在函数内部,使用输出流
out
来格式化并输出Date对象的年、月、日信息。 - 返回输出流引用:为了支持链式输出,函数应返回
ostream
的引用。
注意事项:
- 在使用流插入 << 时,要确保第一个参数是ostream的对象,如果是在重载的时候写成成员函数,编译器就会将第一个参数默认传入this指针,因此只能将其重载成全局函数,传入两个参数,第一个为ostream的对象,第二个为Date类对象。
- 相应的,重载成了全局函数必然会导致类中的私有成员不能被访问,那么这又该如何呢?这里有两种解决办法:
- 创建公有的成员函数
getYear
,getMonth
,getDay
,这样就可以通过这些函数去访问私有成员变量,java很喜欢使用这种方式。 - 使用友元函数,当一个函数或类被声明为另一个类的友元时,它可以访问该类的所有成员,包括私有成员。
- 创建公有的成员函数
既然有流插入 >> 运算符的重载,那么必然就有流提取 >> 运算符的重载。
- 在C++中,对于自定义类型可以重载流提取 >> 来实现像内置类型一样的输入操作。
cin 是一个 istream 类型的对象。
// 流提取 >> 重载
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请依次输入年 月 日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckInvalid())
{
cout << "亲,你输入了无效日期,请重新输入!" << endl;
}
else
{
break;
}
}
// 返回输入流引用以支持链式操作
return in;
}
【思路】
- 定义重载函数:定义一个函数,接受一个
istream
引用和一个Date&
引用作为参数。 - 输入日期:从输入流中读取年、月、日,并尝试赋值给Date对象。
- 验证日期:调用Date对象的
CheckInvalid
方法来检查输入的日期是否有效。 - 错误处理:如果日期无效,则输出错误消息并继续循环,要求用户重新输入。
- 返回输入流引用:如果日期有效,则退出循环并返回输入流的引用,以支持链式输入。
5. 日期类源码
5.1 Date.h文件
#pragma once
#include <iostream>
using namespace std;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) const;
// 检查日期是否合法
bool CheckInvalid();
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!CheckInvalid())
{
cout << *this << "构造日期非法" << endl;
exit(-1);
}
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
~Date()
{
//cout << "~Date()" << endl;
}
// 取地址操作符重载
Date* operator&()
{
return this;
//return nullptr;
}
// const取地址操作符重载
const Date* operator&()const
{
return this;
//return nullptr;
}
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
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;
// 日期-日期 返回天数
int operator-(const Date& d);
// 打印,const成员函数修饰的是成员函数隐含的this指针,表明不能修改任何成员变量
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 友元声明,可以在类的任何位置
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
// 类外声明
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
5.2 Date.cpp 文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
// 获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
static int days[13] = { 0,31,28,31,30,31,30,31,30,31,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
return days[month];
}
// 日期+=天数
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
if (_month == 12)
{
_month = 1;
_year++;
}
else
{
_month++;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day)
{
Date temp = *this; // 调用拷贝构造,因为temp不是一个已存在的对象
temp += day;
return temp;
}
// 日期-天数
Date Date::operator-(int day)
{
Date temp = *this;
temp -= day;
return temp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
while (day >= _day)
{
day -= _day;
if (_month == 1)
{
_month = 12;
_year--;
_day = GetMonthDay(_year, _month);
}
else
{
_month--;
_day = GetMonthDay(_year, _month);
}
}
_day -= day;
return *this;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date temp = *this;
*this += 1;
return temp;
}
// 后置--
Date Date::operator--(int)
{
Date temp = *this;
*this -= 1;
return temp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
{
if (_day > d._day)
return true;
}
}
return false;
}
// ==运算符重载
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);
}
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
int flag = 1;
Date maxDate = *this;
Date minDate = d;
if (*this < d)
{
flag = -1;
maxDate = d;
minDate = *this;
}
int n = 0;
while (maxDate != minDate)
{
minDate++;
n++;
}
return flag * n;
}
// 检查日期是否合法
bool Date::CheckInvalid()
{
if (_year <= 0
|| _month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
return true;
}
// 流插入 << 重载
ostream& operator<<(ostream& out, const Date& d)
{
// 输出格式化字符串到输出流
out << d._year << "-" << d._month << "-" << d._day << endl;
// 返回输出流引用以支持链式操作
return out;
}
// 流提取 >> 重载
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请依次输入年 月 日:>";
in >> d._year >> d._month >> d._day;
if (!d.CheckInvalid())
{
cout << "亲,你输入了无效日期,请重新输入!" << endl;
}
else
{
break;
}
}
// 返回输入流引用以支持链式操作
return in;
}
总结
🔥💖 此日期类是一个非常适合大家训练自己对运算符重载知识理解和掌握的小项目,它是C++引用、传值/传引用返回、拷贝构造、赋值重载、运算符重载、const成员函数、const修饰参数等知识的一个融合,相信大家在上手练习的过程中能收获颇丰。