类和对象(中)——日期类的实现取地址运算符重载
这篇文章笔者先带领读者实现一下日期类,然后学习类和对象中的取地址运算符重载。源码依旧放在github上:C-/Date at main · z-yi-han/C-
日期类的实现
首先,笔者带领读者从0开始实现日期类,实现日期类是对前面所学的运算符重载函数的一个综合运用,难度较高,内容较多,希望笔者认真跟紧读者的节奏。
普通运算
首先,是我们完成一项工程的老规矩,声明定义分离,我们先明确需要的函数:因为日期类的特殊性,我们不需要写出析构函数和拷贝构造函数,所以我们只需要声明加减乘除加等减等这些内容即可,下面笔者把声明展示一下:
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1);void Print();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);Date operator+(int day) const;Date& operator+=(int day);Date operator-(int day) const;Date& operator-=(int day);// d1++;// d1.operator++(0);Date operator++(int);// ++d1;// d1.operator++();Date& operator++();// d1--;// d1.operator--(0);Date operator--(int);// --d1;// d1.operator--();Date& operator--();// d1 - d2int operator-(const Date& d) const;
private:int _year;int _month;int _day;
};
如上面的代码所写,首先笔者声明了一个构造函数用来初始化Date,然后就是经典的Print,下面就是类中的大头戏——运算符重载,因为Date是自定义,因此只能进行运算符重载让他们计算,这其中重载的种类也有不同:首先是判断大小的符号:
比较运算符
>,<,==,这些符号均是比较(也是双目 )运算符 ,而且并不会改变前后两个变量的值,根据之前学的知识,我们知道了,一个成员函数(类内)默认第一个参数是一个this指针,所以在传参的时候只需要传入后面的参数即可,这也就是声明的时候为什么括号里只需要写const Date &d,在实现小于之前,我们先做一些准备工作:1是写一个验证日期是否符合正常的函数,二是获得每个月天数的函数GetMonthDay:
Date(int year = 1900, int month = 1, int day = 1);void Print();// Ĭinlineint GetMonthDay(int year, int month) const{assert(month > 0 && month < 13);static int monthDayArray[13] = { -1, 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;}return monthDayArray[month];}
bool Date:: CheckDate()
{if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)){return false;}else{return true;}
}
下面开始写<的运算符重载:
bool Date::operator<(const Date& d)
{if (_year < d._year){return true;}else if (_year == d._year){if (_month < d._month){return true;}else if (_month == d._month){return _day < d._day;}}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);
}
bool Date::operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;
}bool Date::operator!=(const Date& d)
{return !(*this == d);
}
基于<即可,因为<已经写好了。
算数运算符
下面我们来写算数运算符:也就是+,+=,-,-=这四个,还是老样子,我们用一组+,+=举例即可,首先实现+=:
Date& Date::operator+=(int day)
{_day += day;while (_day>GetMonthDay(_year,_month)){++_month;while (_month == 13){++_year;_month = 1;}}return *this;
}
其实具体的实现就不难了,不过要注意参数,第一个参数是this指针默认不写,但是函数体中是可以写的,因此直接返回this指针即可,根据+=,其实不难写出+因为二者最大的区别就是前面的参数是否改变,也就是this指针指向的参数是否改变:
Date Date::operator+(int day)
{Date tmp = *this;tmp += day;
}
剩下的两段代码直接写下来就可以,笔者不作过多赘述,而且我们还需要注意输入的参数是负数的情况,这种情况直接让其变号即可,笔者在这里把这四个运算符的最终形式放一起:
Date Date::operator+(int day)
{Date tmp = *this;tmp += day;
}
Date& Date::operator+=(int day)
{if (day < 0){return *this -= (-day);}_day += day;while (_day>GetMonthDay(_year,_month)){++_month;while (_month == 13){++_year;_month = 1;}}return *this;
}
Date Date::operator-(int day)
{Date tmp = *this;tmp -= day;return tmp;
}// d1 -= 100
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){--_month;if (_month == 0){_month = 12;--_year;}_day += GetMonthDay(_year, _month);}return *this;
}
自增自减运算符
下面我们来完成自增自减运算符的重载,首先要知道 ++有前置和后置之分,但是在重载的时候我们看不出来是前置还是后置,因此C++语法中硬性规定了一点:后置++重载符中传入一个int作为占位符,并无其他实际意义,至于具体的实现就很简单了,笔者不过多赘述:
// d1++;
// d1.operator++(0);
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}// ++d1;
// d1.operator++();
Date& Date::operator++()
{*this += 1;return *this;
}
Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}// ++d1;
// d1.operator++();
Date& Date::operator--()
{*this -= 1;return *this;
}
作差
最后呢,我们完成两个日期作差,当然单位一定是天,这个操作需要我们思考一下如何写一个最优化的代码,自然想法当然是求差的年份月份天数,但是这样操作实在是过于麻烦,因此我们尝试用一种新的算法:尝试不断比较两个日期的大小,因为比较运算符已经重载完成,而且传入的应该是两个d,因此对this指针和传入的d讨论即可,这时候我们不难想到之前用到的假设思想 ,然后用一个计数器即可,最后还可以引入一个flag来判断正负:
/ d1 - d2
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){++min;++n;}return n * flag;
}
流插入流提取重载
下面我们学习一个难点——流插入流提取的重载。首先在进行之前我们需要复习一下cin,cout,众所周知在C++cin,cout可以自动识别类型进而不需要像C语言那样用占位符,究其根源是因为C++语言已经封装好了,我们可以在Cplusplus官网中查到他们的底层逻辑:cplusplus.com - C++资源网络。当然,我们的目的是实现六输入流提取的运算符重载,因为其实cin,cout也只支持内置类型,用print,scanf又太麻烦。首先我们按照常理将其放进类中作为成员函数,然后我们在实现流提取:
void operator <<(const Date& d);
void operator >>(const Date& d);
void Date::operator <<(const Date& d)
{}
void Date::operator >>(const Date& d)
{}
这时候如果我们测试一下不难此时是不对的,想对的话就得换方向,这是为什么呢?首先我们需要回忆一下类中 成员函数的特点,在类中写的成员函数默认第一个参数位置是一个this指针(具体可以看笔者之前的文章),如果这时候正向调用,this指针指向的是cout,参数是d,this指向cout,cout的类型是std::ostream,但是标准库我们无法修改,因此这样就会报错,所以如果想要输入正确那么就需要写反,显然,这种形式是反人类的,因此我们需要尝试另一种方式完成重载,既然是因为this指针才不对,所以干脆就不写成员函数,我们直接写成全局函数即可,直接在外面声明,这样的话重载就毫无问题了,下面笔者直接给读者看一看自己的代码:
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.CheckDate()){cout << "输入日期非法:";d.Print();cout << "请重新输入!!!" << endl;}else{break;}}return in;
}
这时候其实还有一个非常严重的问题,那就是私有问题,_day,_year,_month都是私有的,不能把他们公有化,写一个获取函数的话又过于麻烦,因此,我们引入一个新的方法——友元函数,也就是直接声明friend即可:
friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);
这样问题就迎刃而解了,到这里其实日期类就完成了,下面剩下的就是一些测试工作了。
测试
测试中笔者就不过多赘述了,笔者直接放出代码:
#include <iostream>
#include "Date.h"
using namespace std;//-----------------------------------
// 测试函数区
//-----------------------------------// 测试比较运算符
void TestCompare()
{cout << "===== TestCompare =====" << endl;Date d1(2024, 5, 20);Date d2(2024, 6, 1);cout << (d1 < d2) << " // <" << endl;cout << (d1 <= d2) << " // <=" << endl;cout << (d1 > d2) << " // >" << endl;cout << (d1 >= d2) << " // >=" << endl;cout << (d1 == d2) << " // ==" << endl;cout << (d1 != d2) << " // !=" << endl;cout << endl;
}// 测试 += 和 + 运算符
void TestPlus()
{cout << "===== TestPlus =====" << endl;Date d1(2024, 12, 25);Date d2 = d1 + 10; // 调用 operator+cout << "原日期: " << d1;cout << "+10天后: " << d2;d1 += 40; // 调用 operator+=cout << "再 +40天: " << d1;cout << endl;
}// 测试 -= 和 -(天数)
void TestMinus()
{cout << "===== TestMinus =====" << endl;Date d1(2024, 3, 10);Date d2 = d1 - 15; // 调用 operator-cout << "原日期: " << d1;cout << "-15天后: " << d2;d1 -= 40;cout << "再 -40天: " << d1;cout << endl;
}// 测试 日期 - 日期(相差天数)
void TestSubDate()
{cout << "===== TestSubDate =====" << endl;Date d1(2024, 1, 1);Date d2(2024, 1, 15);cout << d2 - d1 << " 天" << endl;cout << d1 - d2 << " 天" << endl;cout << endl;
}// 测试 ++ 和 --
void TestIncDec()
{cout << "===== TestIncDec =====" << endl;Date d1(2024, 2, 27);cout << "初始: " << d1;cout << "后置++: " << d1++;cout << "执行后: " << d1;cout << "前置++: " << ++d1;cout << "执行后: " << d1;cout << "后置--: " << d1--;cout << "执行后: " << d1;cout << "前置--: " << --d1;cout << "执行后: " << d1;cout << endl;
}// 测试输入输出运算符
void TestIO()
{cout << "===== TestIO =====" << endl;Date d;cin >> d; // 触发 operator>>cout << "你输入的日期是: " << d; // 触发 operator<<cout << endl;
}//-----------------------------------
// 主函数
//-----------------------------------
int main()
{TestCompare();TestPlus();TestMinus();TestSubDate();TestIncDec();// TestIO(); // 最后手动测试输入输出时再打开cout << "所有测试完毕!" << endl;return 0;
}
取地址运算符重载
日期类实现之后呢,我们学习取地址运算符的重载,因为这种运算符比较特殊,所以单独介绍,当然了,剩下的内容就比较简单了,本文的重点还是在上边的内容。
const成员函数
首先我们先介绍一下const成员函数,其实这种函数是一种补丁行为,众所周知,在成员函数中,默认带了一个this指针,但是如果我们要求第一个参数不能改变,也就是this指向的参数不能动的时候,按照常理我们应该在this指针前面,但是我们都知道this指针不能在参数列表显示,所以为了打补丁,就在括号后面加一个const,这只是一个特殊格式,读者记住即可,介绍完const相信读者也能想到对Date类的一些优化,比如打印函数,我们都知道打印的时候必须保证输入的值不变,还有运算符重载的时候的+,第一个参数同样不希望改变,基于此,笔者对日期类进行了改善:
#include"Date.h"
Date::Date(int year, int month, int day )
{_year = year;_month = month;_day = day;
}
bool Date:: CheckDate() const
{if (_month < 1 || _month > 12|| _day < 1 || _day > GetMonthDay(_year, _month)){return false;}else{return true;}
}
void Date::Print() const
{cout << _year << "/" << _month << "/" << _day<<endl;
}
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){return _day < d._day;}}return false;
}
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 _year == d._year&& _month == d._month&& _day == d._day;
}
bool Date::operator!=(const Date& d) const
{return !(*this == d);
}Date Date::operator+(int day) const
{Date tmp = *this;tmp += day;return tmp;
}
Date& Date::operator+=(int day)
{if (day < 0){return *this -= (-day);}_day += day;while (_day>GetMonthDay(_year,_month)){++_month;while (_month == 13){++_year;_month = 1;}}return *this;
}
Date Date::operator-(int day) const
{Date tmp = *this;tmp -= day;return tmp;
}// d1 -= 100
Date& Date::operator-=(int day)
{if (day < 0){return *this += (-day);}_day -= day;while (_day <= 0){--_month;if (_month == 0){_month = 12;--_year;}_day += GetMonthDay(_year, _month);}return *this;
}// d1++;
// d1.operator++(0);
Date Date::operator++(int)
{Date tmp = *this;*this += 1;return tmp;
}// ++d1;
// d1.operator++();
Date& Date::operator++()
{*this += 1;return *this;
}
Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}
// ++d1;
// d1.operator++();
Date& Date::operator--()
{*this -= 1;return *this;
}// d1 - d2
int Date::operator-(const Date& d) const
{int flag = 1;Date max = *this;Date min = d;if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){++min;++n;}return n * flag;
}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.CheckDate()){cout << "输入日期非法:";d.Print();cout << "请重新输入!!!" << endl;}else{break;}}return in;
}
取地址运算符重载
下面就是本文最后的内容——取地址运算符的重载。取地址运算符分为普通取地址运算符重载和const取地址运算符重载,这两种取地址是有些区别的,不能一概而论用const修饰。这里的const修饰的是指针,带const的版本只能被常量对象调用,而不带的版本用于非常用对象,当然了,在实际工程中,我们并不需要写,编译器自动生成即可。
