目录
一、前言
前边基于对类和对象的认识,本章将以日期计算器为例,通过C++程序一步一步操作实现。如图所示:日期计算器要实现计算日期差和推算具体日期两种功能。

二、日期计算器的功能需求
- 检查日期是否合理;
- 两个日期之间的大小比较;
- 实现当前日期加减天数后的日期计算功能;
- 实现两个日期之间相差多少天的计算功能;
- 自定义打印。
三、日期计算器的分步实现
假设程序实现在Date类类型中,且private中定义了int _year,int _month,int _day。基于上述需求:我们可以分步骤依次实现:
3.1 日期有效性确定
int GetMonthDay(int _year, int _month)
{
assert(_month < 0 && _month > 13);
const int _GetMonthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if(_month == 2)
{
if(((_year % 4 == 0) && (_year % 100 != 0)) || (_year % 400 == 0))
{
return 29;
}
}
return _GetMonthDay[_month];
}
bool CheckInvalid()
{
if (_year <= 0
|| _month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
3.2 日期之间的大小比较
日期之间的大小比较分为一下几种情况:(1)Date1 > Date2;(2)Date1 == Date2;(3)Date1 < Date2。因此,自定义类型需要运算符重载:
bool operator>(const Date& d)
{
if(_year < d._year)
{
return false;
}
else if(_year == d._year)
{
if(_month < d._month)
{
return false;
}
else if(_month == d._month)
{
if(_day < d._day)
{
return flase;
}
}
}
return true;
}
bool operator<(const Date& d)
{
return !(*this > d);
}
bool operator==(const Date& d)
{
return _year == d._day
&& _month == d._month
&& _day == d._day;
}
bool operator!=(const Date& d)
{
return !(*this == d);
}
3.3 当前日期加减天数
3.3.1 日期加天数
Date operator+(int day)
{
Date tmp(*this);
tmp._day += day;
while(tmp._day > GetMonthDay(_year, _month))
{
tmp._day -= GetMonthDay(_year, _month);
tmp._month++;
if(tmp._month == 13)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
tmp出了作用域就销毁,所以不能使用引用返回,必须使用传值返回。
此外,当把程序Date tmp(*this);改为Date tmp = *this;,此时,这个代码是拷贝构造还是赋值? - 拷贝构造
原因在于:赋值针对的是两个已经存在的对象,这里虽然有赋值符号,但是这里的tmp并不是已经存在的对象,而是用一个已经存在的对象*this也就是d1,去初始化另一个正在构造的对象tmp,所以此处更符合拷贝构造的概念,而不能被形式迷惑了。
基于上述程序,我们可以做一个沿伸:
(1)通过operator+,实现operator+=:
Date& operator+=(int day)
{
return *this + day;
}
(2)通过operator+=,实现operator+:
Date& operator+=(int day)
{
_day += day;
while(_day > GetMonthDay[_year, _month])
{
_day -= GetMonthDay[_month];
_month++;
if(_month > 12)
{
_year++;
_month = 1;
}
}
return *this;//d1出了作用域还要存在,所以要用引用返回
}
Date operator+(int day)
{
Date tmp(*this);
return tmp += day;
}
综上:引用返回可以减少拷贝,这里返回的就是*this,即d1。而传值返回,返回的是tmp的拷贝,不会返回tmp,因为出了作用域会销毁。也就是引用返回效率更高,但具体要看返回值出了作用域还在不在了。
哪些是出了作用域还在的呢?全局对象、静态对象。但实践当中,比如*this不是全局对象,也不是静态对象,就是d1,只是说d1的生命周期不在+=里边,是在+=外边一层。
比较上述两组程序哪个更好? - 右边

如图所示:我们可以对应看,即+=对应+=,+对应+。
+对应+:
右边:复用+=的逻辑,因为是引用返回,所以复用是没有代价的,即没有产生对象;左边:自己实现+的逻辑。所以当前来看是等价的,效果一样。 所以没有效率上的差别,左右两边都产生了tmp。
+=对应+=:
右边:没有产生临时对象;左边:+=中的*this + day复用+,会产生tmp临时对象,此外传值返回会再产生一个临时对象。左边的+=复用+,+里边要创建对象。
3.3.2 日期减天数
基于上述结论,我们在-中复用-=来实现日期减天数:
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
3.3.3 日期前置++
operator为单操作符,这样编译器可能无法有效区分前置++和后置++,逻辑不自洽,所以要特殊处理。
前置++,返回的是递增后的日期对象的引用,如:++d1;。通过调用运算符重载,先将当前日期+1,然后返回自增后的对象,可用传引用返回。
//++d -> d.operator++()
Date& operator++()
{
*this += 1;
return *this;
}
3.3.4 日期后置++
为了跟前置++区分,强行增加一个int形参,构成重载区分。
后置++,返回的是递增前的日期对象,如:d1++;。通过调用运算符重载,先拷贝一份临时的日期对象tmp,将当前日期+1,然后返回拷贝的临时日期对象tmp,当函数生命周期结束时,tmp会销毁,所以只能用传值返回。
//d++ -> d.operator++(0)//传的值是随机的,只是为了匹配小括号中的int
Date operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
所以在自定义类型中,前置++效率高,后置++一方面创建临时变量,另一方面传值返回要调用一次拷贝构造。
3.4 日期之差
日期减日期(d1 - d2)得到天数,正值代表当前日期与之前相差多少天,负值代表当前日期与之后相差多少天。那么如何使d1拿到大日期,d2拿到小日期呢?此处不妨直接假设d1 > d2,复用运算符重载>,如果当d1 < d2时,调换对象d1和d2中的内容。
此外,日期之差不仅是_day相减,也包括_year,_month。直接相减过于复杂,不妨换个思路,让小日期补充n天后等于大日期,此时,n就是日期之差。
我们可以在此复用前置++。所以,日期之差的程序如下:
int 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(max != min)
{
++min;
++n;
}
int ret = flag * n;
return ret;
}
3.5 自定义打印
C++使用流插入是要解决所有类型的打印问题。printf虽然可以支持int float等内置类型,但无法支持自定义类型。printf("%d\n", i);。
C++规定,自定义类型想用运算符,都要运算符重载。所以,内置类型都可以走流插入,自定义类型不可以。
cout << d;//err
流插入运算符有两个操作数:1.cout;2.d1。作为成员函数重载,this指针占据第一个参数,Date d1必须是左操作数了。此时:
void operator<<(ostream& out);
/////////////////////////////////////////
cout << d1;//err,报错了,如果是双操作数,参数类型要匹配
也就意味着,调用运算符重载,其实是下边的形式:
d1.operator<<(cout);
///////////////////////////////////////////////////
d1 << cout;//变成了控制台流入到日期类对象中去了。
因为写成成员函数,就控制不了第一个位置。如果我们要让cout传给ostream占据第一个参数,这时operator就不能写成成员函数,而是要写成全局的。全局函数不能在.h中定义,可以内联,也可以声明和定义分离。
基于此,cin >> d2 >> d1;和cout << d1 << d2;的程序如下:
注意:
此处结合性,从左向右,先cout << d1,这个表达式实际上是一个函数调用,有一个返回值,返回值作为左操作数再进行流插入。所以把返回类型由void改为ostream&。
ostream& operator<<(ostream& out, const Date& d)//传引用,out就是cout的别名
{
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;//支持连续的流提取,所以必须要有一个返回值
}
还有两处需要注意:
- 写为全局函数访问不了私有private,可以增加友元声明;
- 流提取Date不能加const,因为提取的值要放在日期类中。
四、日期计算器的程序
4.1 Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include"Date.h"
int main()
{
Date d1(2025, 2, 28);
Date d2 = d1 + 20;
cin >> d1 >> d2;
cout << d1 << d2 << endl;
return 0;
}
4.2 Date.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
bool CheckInvalid();
Date(int year = 1, int month = 1, int day = 1);
bool operator<(const Date& d);
bool operator>(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date operator-(int day);
Date& operator-=(int day);
Date& operator++();
Date operator++(int);
int operator-(const Date& d);
//本质是inline
int GetMonthDay(int year, int month)//函数写成内联,即不声明定义分离
{
assert(month > 0 && month < 13);
static int monthDays[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;
}
return monthDays[month];
}
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
4.3 Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
Date::Date(int year, int month, int day)//直接在类中定义默认就是内联函数,即使不加inline也是内联
{
_year = year;
_month = month;
_day = day;
if (!CheckInvalid())
{
cout << "构造的日期非法" << endl;
}
}
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)
if (_day < d._day)
{
return true;
}
}
return false;
}
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)
{
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
Date& Date::operator+=(int day)
{
*this = *this + day;
return *this;
}
//++d -> d.operator++()
Date& Date::operator++()
{
*this += 1;
return *this;
}
//d++ -> d.operator++(0)//传的值是随机的,只是为了匹配小括号中的int
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
int flag = -1;
max = d;
min = *this;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
bool Date::CheckInvalid()
{
if (_year <= 0
|| _month < 1 || _month > 12
|| _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
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;//支持连续的流提取,所以必须要有一个返回值
}


3880

被折叠的 条评论
为什么被折叠?



