当前位置:   article > 正文

【C++】日期类的实现——运算符重载_使用重载运算符(++,+=,<<等)实现日期类的操作。功能包括:1)设置日期,如果日期设置

使用重载运算符(++,+=,<<等)实现日期类的操作。功能包括:1)设置日期,如果日期设置

简介

在C++中,为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型、函数名字以及参数列表等内容,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
但也要注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  5. .*、::sizeof、三目运算符?:和点运算符. 5种运算符不能重载。这个经常在笔试选择题中出现。

下面我们进入日期类:

类的定义

class Date
{ 
public:
 Date(int year = 1970, int month = 1, int day = 1)
   {
        _year = year;
        _month = month;
        _day = day;
   }    
private:
 int _year;
 int _month;
 int _day;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

初始类只是一个简单的框架,这里构造函数我们是给的全缺省,它是一个默认构造函数。下面我们开始做日期类的内容:

一些默认成员函数

有类就有默认成员函数,上面我们已经写好了构造函数,这里主要写拷贝构造函数和析构函数(日期类其实也不用写这个)。

拷贝构造函数

Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

拷贝构造函数会在我们进行拷贝定义时进行调用,具体调用方法为:

classname old_member(new_menber);
  • 1

由于日期类比较简单,这里只是对于新对象的直接赋值。此外,因为old_member的成员不会发生变化,所以这里我们选择用const来进行修饰。

析构函数

一般来说,日期类中成员的存储无需向堆申请内存,所以我们没有什么必要来重新写这个析构函数,类中写好的就足够了,但只是针对这个类而言是这样,这里主要是提一下这个默认成员函数,因为它也很重要。如果非要写也可以写成下面这样,但多少有点画蛇添足的意思:

Date::~Date()
{
	_year = 0;
	_month = 0;
	_day = 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

下面正式进入运算符重载环节。

赋值运算符重载

先贴代码:

Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
		
	return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

所谓赋值运算符也就是”=“,对于内置类型我们可以直接使用等号来进行赋值,但是类对象就不行。因此我们对于赋值运算符进行重载,而后就可以使用一个定义好的类对象对于另一个类对象进行赋值。

这里的实现也没有很难,就是进行值的传递,然后返回接收好数据的对象即可。这里赋值的发出者没有进行改变,所以我们选择用const进行修饰;返回后的*this对象并没有被释放掉,所以我们选择返回引用来减少拷贝。

日期 += 运算符重载

在生活中,我们经常会说:一百天之后是哪天?这时候我们就需要这样的功能来回答这个问题。对于一个日期类对象加上一定的天数,求出这么多天之后是哪天。代码如下:

int Date::GetMonthDay(int year, int month)
{
	static int day_num[13] = { 0,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;
	}
	else
	{
		return day_num[month];
	}
}

Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

这个功能之中最棘手的就是处理进位问题,日超过当月上限要进位,月超过13要进位,后者相对来说还比较简单,但是日期参差不齐的月属实让人头疼,所以这里我们专门写了一个能够求每个月有多少天的函数。因为有闰年的存在,这个函数必须是两个参数,也就是也要把年传进去。我们在函数中定义一个静态数组,写好每个月的日期,当符合闰年加二月时,返回29,其他情况返回对应月份的天数即可。

回头看+=功能就没那么复杂了,我们先将要加的天数加给day,如果未超出当月上限,可直接得出结果,否则进入循环:
①先使day减去当前月份的天数,然后使month++。
②然后判断month是否超过12,若超过,则使年++,并将月置为1。
③然后继续判断day是否超过了当月天数上限,循环上述步骤。
④因为我们默认年是没有上限的,所以以上循环就已经解决问题了。
⑤如果你问我给你传负数怎么办,那我也没什么办法,一会儿我会实现-=的运算符重载,您不介意可以用这个,如果您非要用+=输入负数,那我真没辙。

日期+运算符重载

代码如下:

Date Date::operator+(int day) const
{
	Date ret(*this);//拷贝构造函数
	ret += day;
	return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里我们能复用肯定是选择复用的。因为加运算并不会改变左操作数的值,所以这里我们选择const进行修饰;又正因如此,我们会返回一份临时拷贝,而非左操作数本身,所以类型是Date。

日期-=运算符重载

我们如果会写+=,那这个自然也不会难住你。代码如下:

Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month <= 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

逻辑和上面相似:
①先给day减去对应天数,如果仍为正数,那么直接返回结果
②如果为负数,那就要向上个月进行借位,我们先直接给月份减去一,然后判断月份是否已经小于1了,如果小于一了那么就还要向年借位。
③都借好了之后,给day加上上个月的日期,然后继续判断day是否为正数。
④这里没有考虑公元前,如果你觉得有必要可以自行修改。
⑤如果你也想输入负数,那当然可以,我又没说我这个程序健壮性很强。

日期-运算符重载

这里我们仍然秉持能复用就复用原则:

Date Date::operator-(int day) const
{
	Date ret(*this);//拷贝构造函数
	ret -= day;
	return ret;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

由于减操作不改变左操作数,所以返回一份拷贝,并给左操作数加好const。

前置++ & 后置++

我们知道,前置++在表达式中会先++然后再使用,而后置++则正好相反。此外,虽然我们知道二者的区别,但是在写运算符重载的时候我们并不能将二者很好的区别开:

Date operator++();//参数是隐藏的this
  • 1

所以C++的编写者为了能够将二者区别开对此进行了规定:后置++的运算符重载要加一个int类型的参数,且不必写形参名,写一个类型即可。所以后置++可以写成:

Date operator++(int);//这就是后置++了
  • 1

能将二者很好的区别开之后我们就可以实现了:

// 前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
// 后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

他们两个作用上的区别大家都很清楚,这里我们主要说说返回值类型的问题。前置++返回的是加后的日期,所以我们直接返回引用就可以,因为我们对日期本身进行了++,这里能减少拷贝构造的调用;但对于前置++来说,我们虽然也要对其++,但是返回的必须是++之前的,所以要进行拷贝并返回++之前的日期。这里我们也是复用了前面的+=代码,不难看出,复用能给我们省很多事~

前置–和后置–

道理和前面一致,加一个参数来区分二者。代码如下:

// 后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}
// 前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

思想比较简单,就不赘述了。

==运算符重载

相等的判断非常简单:

// ==运算符重载
bool Date::operator==(const Date& d) const
{
	return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}
  • 1
  • 2
  • 3
  • 4
  • 5

>运算符重载

比较日期大小对于我们来说是一目了然的事情,但是具体用代码进行实现还是要稍微思考一下的:

// >运算符重载
bool Date::operator>(const Date& d) const
{
	if (_year > d._year)
	{
		return true;
	}
	else if (_year == d._year && _month > d._month)
	{
		return true;
	}
	else if (_year == d._year && _month == d._month && _day > d._day)
	{
		return true;
	}

	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这里用三次if判断来确定比较结果:年大的日期肯定大;年一样月大的日期肯定大;年月都一样日大的日期大,如果这三个判断条件都跳过了,说明要么年小,要么月小,要么日小,自然也就是比较小的日期。

>=运算符重载

复用、复用、还是**的复用:

bool Date::operator>=(const Date& d) const
{
	return *this > d || *this == d;
}
  • 1
  • 2
  • 3
  • 4

<运算符重载

小于和小于等于的实现思路和上面基本一致,这里就不多说了:

bool Date::operator < (const Date& d) const
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month)
	{
		return true;
	}
	else if (_year == d._year && _month == d._month && _day < d._day)
	{
		return true;
	}

	return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

<=运算符重载

// <=运算符重载
bool Date::operator <= (const Date& d) const
{
	return *this < d || *this == d;
}
  • 1
  • 2
  • 3
  • 4
  • 5

!=运算符重载

不相等嘛,就是相等取反咯:

// !=运算符重载
bool Date::operator != (const Date& d) const
{
	return !(*this == d);
}
  • 1
  • 2
  • 3
  • 4
  • 5

日期-日期中的减号运算符重载(返回int)

这个功能的实现方法有很多,这里讲解一个思路非常简单的方法:我们前面已经实现了日期类对象的++和==两个运算符,所以我们用前面的>运算符找到比较小的日期,然后给它一直++,与此同时定义一个计数器,小日期++的同时他也跟着++,每次++之后都拿来和较大的日期进行比较,直到二者相等,返回计数器中的值即可。实现方法如下:

// 日期-日期 返回天数
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	int n = 0;

	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	while (min != max)
	{
		++n;
		++min;
	}
	return n * flag;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这里我们定义了一个flag,并且默认认为左值为较大值,右值为较小值,若实际情况与此正好相反,那我们就将二者进行调换,并将flag置为-1。随后开始计数环节。因为我们这里是左值减右值,如果左值较大,那么flag就是正数,返回的自然也就是正数;否则flag是负数,也就会返回一个负数。

<<(输出流符号)运算符重载

我们如果想要打印日期类,可以通过在类中定义Print方法,然后通过下面的语句进行打印:

cout << _year << "年" << _month << "月" << _day << "日" << endl;
  • 1

但是这种方法非常的麻烦,我们还要调用一个函数,如果我们有调试的需求,走一遍Print函数更是烦得很,所以我们这里考虑,为什么不能直接对于流插入符号进行重载,使日期类的某个对象直接流向cout函数呢?

cout << d1 << endl;//直接将对象流给cout
  • 1

这个方法的实现里面就涉及到很多知识点了,我们一一道来,首先讲讲:

cout函数的类

在这里插入图片描述
这幅图里面还涉及到了很多继承派生等进阶概念,我们先不展开,但是我们能清楚的看到cout函数是ostream类中的一个方法,现在我们知道了cout的类型,可以开始动手写了。
版本一:

//版本一
class Date
{ 
public:
	void operator<<(ostream& myout)
	{
		//...
	}
//...  
private:
 int _year;
 int _month;
 int _day;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

版本一中我们将这种方法直接写进了类里面,又因为所有类方法的参数都默认带有一个this指针,所以这里我们只能在填一个ostream&类型的流插入对象。但是此时就出现了一个问题,正常来讲,我们再调用cout方法时,一般的格式为:

cout << d1 << endl;
  • 1

但是我们这里的第一个参数为隐藏的this指针,也就是Date*类型的对象,如果想要正确的打印,我们就不得不这样写:

d1 << myout;
  • 1

所以这样看起来会不会很诡异?本来是人骑着马,现在变成马骑着人了(bushi)。但是我们还解决不了,因为写在类里这个隐藏的this指针我们无法操作,所以我们不得不忍痛割爱将这个运算符重载的方法拿出来,不放在类里面,在类里声明一下,在其他文件中定义,以防止:

重定义问题

注意,我们在外面定义函数的时候在类里面留了一个函数声明,并使声明和定义分离,在其他文件中对函数进行定义,这是为什么呢?因为有类定义的文件大多都是.h的文件,会被其他的cpp文件进行包含,如果我们在函数实现的Date.cpp文件和主函数test.cpp文件中同时包含了此.h文件,那么在进行编译的时候,就会将这个函数的定义分别拷贝到两份cpp文件之中,此时就会导致函数重定义问题,所以这里我们要先将写在类外的函数在类里面先声明,然后在其他地方进行定义,从而避免这个问题。此外,如果不想让二者分离,我们还可以直接将这个函数改为静态的,这样他只能为本文件所用,无法进入符号表,这样也能直接解决问题。

然后我们将代码改成酱紫:

//版本二
class Date
{ 
public:
	void operator<<(ostream& myout, const Date& date);

//...  
private:
 int _year;
 int _month;
 int _day;
};


//另一个文件
void operator<<(ostream& myout, const Date& date)
{
	myout << date._year << "年" << date._month << "月" << date._day << "日" << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

写到类外我们确实把顺序扳回来了,但是又出现了新的问题,你在类外面,怎么用类对象访问私有属性呢?答案是不能访问,此时C++就有一个能够开后门的选项,也就是

友元函数

友元函数,是指某些虽然不是类成员却能够访问类的所有成员的函数。 类授予它的友元特别的访问权。这样我们就可以在类外直接访问私有成员了。修改之后代码就成了酱紫:

//版本三
class Date
{ 
public:
	friend void operator<<(ostream& myout, const Date& date);//友元函数声明

//...  
private:
 int _year;
 int _month;
 int _day;
};


//另一个文件
void operator<<(ostream& myout, const Date& date)
{
	myout << date._year << "年" << date._month << "月" << date._day << "日" << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这样,我们就能够直接在类外访问到类里面的私有属性了,但是还存在着一定的问题,举个例子:比如我们想打印好几个日期,我们就得这样写代码:

myout << d1 << d2 << d3 << endl;
  • 1

但是当前的函数是没有返回值的,无法完成链式访问,所以要让这个函数能够:

链式访问

只有通过链式访问,才能进行一连串的打印。为了实现它,我们必须修改此函数的返回值:

//版本四
class Date
{ 
public:
	friend ostream& operator<<(ostream& myout, const Date& date);//友元函数声明

//...  
private:
 int _year;
 int _month;
 int _day;
};


//另一个文件
ostream& operator<<(ostream& myout, const Date& date)
{
	myout << date._year << "年" << date._month << "月" << date._day << "日" << endl;
	
	return myout;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这样,在执行了”myout << date._year“这句代码之后,就会返回myout这个对象,然后让它继续去接收后面的输出流,这样我们就可以打印一连串的内容了,也符合一个真正cout的功能。但是,既然我们提到了链式访问,就说明这个函数是可能被频繁调用的(打印函数也确实如此),C++在C的基础上优化了一个非常好用的功能,能够减少某些篇幅较短但是调用次数较多的函数的内存开销,这个方法叫做:

内联函数

内联函数不会进入符号表,和静态函数一样都只能在本文件中进行使用,而且它在被调用的时候不会执行压栈操作,会直接展开,节省了内存的开销,对于频繁调用的函数来说是非常合适的一种属性。此外,我们前面用加粗的字体提到,为了防止重定义,我们将声明与定义分离,或者将其定义为静态函数,其实将其直接定义为内联函数并将其仍放在头文件中也是可以解决这个问题的,这样也是不用将声明定义进行分离的。所以这里我们再把函数”请“回来:

//版本五
class Date
{ 
public:
	friend ostream& operator<<(ostream& myout, const Date& date);//友元函数声明

//...  
private:
 int _year;
 int _month;
 int _day;
};
//同一文件
inline ostream& operator<<(ostream& myout, const Date& date)
{
	myout << date._year << "年" << date._month << "月" << date._day << "日" << endl;
	
	return myout;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

经历了五个版本的更新,我们终于写出了一个像样的输出函数,后面交代一下流输入函数,和上面的实现思路一致。

>>(输入流符号)运算符重载

代码如下:

class Date
{ 
public:
	friend istream& operator>>(istream& myin, const Date& date);//友元函数声明

//...  
private:
 int _year;
 int _month;
 int _day;
};
inline istream& operator>>(istream& myin, Date& date)
{
	myin >> date._year >> date._month >> date._day;

	return myin;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

实现思路和上面一样,这里就不再赘述了,需要说的就是这个cin对象它是istream类型的,要改一下。

结束语

到此为止,关于C++日期类的实现就全部讲完了,里面还有一些CPP其他零散知识点的讲解,希望能够对你有所帮助,如文章有不足或遗漏之处还请大家指正,笔者感激不尽;同时也欢迎大家在评论区进行讨论,一起学习,共同进步!

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号