基于前面两次对类与对象的基本讲解,本次主要通过写一个完整的Date类对前面提到的相关概念的一个更加具体的理解。希望对文中有错误的地方加以指出,感谢!
日期类的实现
-
Date类的基本框架
首先定义Date类的基本框架:
//一个Date类的简单框架,包含年、月、日三个成员变量
class Date
{
//私有成员变量,在类外不能直接访问
private:
int _year;
int _month;
int _day;
}
-
构造函数
上面完成了对类的基本框架的定义之后,我们需要对类中的私有成员变量进行初始化,对于初始化工作,由两种方法,一是通过构造函数,二是通过初始化成员列表:
//一个Date类的简单框架,包含年、月、日三个成员变量
class Date
{
//公有成员函数,在前面提到过访问限定符的作用范围,可以理解为在这里完成
//对私有成员变量的一系列处理,需要注意初始化成员列表与构造函数只能存在一个
public:
//全缺省构造函数初始化,即是在形参后面给缺省值,当创建对象时给参数
//则缺省参数没有用,当没有提供参数时,也不至于出现随机值
Date(int year = 0,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//初始化成员列表,对私有变量的初始化
Date(int year = 0,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
//私有成员变量,在类外不能直接访问
private:
int _year;
int _month;
int _day;
}
-
析构函数
以上是对构造函数的定义,以下析构函数只是为了表明在(二)中提到的析构函数在程序结束时会自动调用。
//对于析构函数的作用,它主要是对我们在堆上申请的空间之后,在我们不使用时
//需要我们对申请的空间进行销毁工作,在这里不太好表现出析构函数的作用
~Date()
{
cout << " 调用析构函数" << endl; //主要是为了表现,在程序结束,的确会自动调用析构函数
}
-
拷贝构造函数及无穷递归调用的理解
拷贝构造函数,拷贝构造函数时拿一个已经存在的对象去创建一个 新的对象。
//拷贝构造函数
//这里const可以加也可以不加,可以简单认为是对对象D1的保护,
//Date D2(D1); 用对象D1拷贝构造新对象D2 ,可以这样看这一句:D2.Date(D1);
//由于这里的参数时Date这个类类型实例化出来的对象D1,因此我们需要用Date类类型
//来接收这个参数,这里的D1传参给了d,在这里必须用引用传参,否则会引发无穷递归调用
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
对于上述代码为什么会引发无穷递归调用呢?这里进行一个简单的解释:
从内存角度来理解一下传值和引用传参,就可以更加容易理解为什么传值会引发无穷递归调用
根据上图,我们可以理解成如果传值传参,就会拷贝——>赋值(为了完成赋值)——>拷贝 ——>赋值(为了完成赋值)……
以上就是我对如果采用传值传参会引发无穷递归调用的原因的理解。
-
运算符重载函数
-
赋值运算符重载 operator=
//operator= d2 = d1
//这里为了方便我们理解,可以看成 d2.operator= (d1) operator=看成整体当做函数名
Date& operator=(const Date& d)
{
//d2 = d2 表示如果自己对自己赋值,则不处理
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
对于以上重载赋值运算符,为什么要用引用传参?又为什么要用引用传返回值呢?
首先,这里采用引用传参,是为了减少拷贝构造,提高效率。那如果运用传值传参会引发无穷递归调用吗? 在这里如果用传值传参并不会引发无穷递归调用,只是会有一次拷贝构造,这里采用引用传参主要就是减少拷贝。提提高效率。
为什么要用引用传返回值呢?我们先来看一下结果:
很明显如果没有用引用传返回值多调用了两次析构函数,这是因为如果没有引用传返回值在执行return *this 时会去调用拷贝构造函数产生一个临时对象。而临时对象出作用域时会进行销毁,也就是会去调用析构函数。前面提到过如果我们没有自定义拷贝构造函数,而是由编译器自动生成拷贝构造函数时,会产生浅拷贝问题(同一块空间被释放两次就会产生崩溃)。如果当我们的对象含有指针类型的成员变量时。如果我们没有使用引用穿返回值,也没有自定义拷贝构造函数时。这是就会造成程序崩溃。因此综上,我们在进行赋值运算符重载时采用引用传返回值,一是为了减少拷贝产生临时对象,提高效率。另一个也是为了保证我们的程序更加安全。
-
日期的比较·
-
operator==
判断两个日期是否相等
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
-
operator>
判断D1是否大于D2
//D1 > D2
bool 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;
}
else
return false;
}
else
{
return false;
}
}
-
operator<
判断D1 是否小于D2,由于上面已经实现了> 和 ==。因此对于这里可以直接复用上面的代码进行实现,小于即即不大于也不等于,逻辑上对> 和 == 取反即可。以下均可带这这样的逻辑进行处理
//D1 < D2
bool operator<(const Date& d)
{
return !(*this > d) && !(*this == d);
}
-
operator >=
判断D1是否大于等于D2 同样的道理,大于等于即是不小于。
//D1 >= D2
bool operator>=(const Date& d)
{
return !(*this < d) ;
}
-
operator<=
//D1 <= D2
bool operator<=(const Date& d)
{
return !(*this > d) ;
}
-
operator!=
//D1 != D2
bool operator!=(const Date& d)
{
return !(*this == d);
}
-
日期的加减运算
日期的加减运算由于不是简单的整形加减运算,它涉及到天满进月,月满进年。因此我们需要先获取月份的天数
//获取月份天数
int GetMonthDay()
{
int _MonthDay[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//如果是闰年二月则为29天
if (_year % 4 == 0 || _year % 100 != 0 && _year % 400 == 0)
{
_MonthDay[2] = 29;
}
return _MonthDay[_month];
}
-
operator+=
这段的逻辑就是,先计算+=之后的天数,如果天数不合法(即天数大于当前月的天数),就对天数进行处理,直到合法
//D1 += day
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay())
{
//注意这里月份进位与天数的处理顺序,如果先将月份进一位,那么天数减去的就是下一
//个月的天数,而我们应该减去的是当前月份的天数
_day -= GetMonthDay();
_month += 1;
while (_month > 12)
{
_month -= 12;
_year += 1;
}
}
return *this;
}
-
operator+
上面实现了日期的+=操作,这里我们实现日期加天数。
//D1 + day
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
这里我们可以看到,我们返回的是一个临时对象,因此不能用引用返回,并且我们处理的是*this创建出来的临时对象,而不是*this本身,这是为什么呢?
区分一下 += 与 + 的区别就很容易明白了,D1 += day, D1自身也会被改变,而D1 + day ,D1自身不会被改变。因此我们要用D1创建临时对象,对临时对象进行处理,而临时对象出作用域会被销毁,因此要返回他的拷贝,而不能用引用返回。
-
operator-=
日期-=日期没什么意义就不写了,就跟上面日期+=日期没有意义类似,因此就不写了。这里写日期-=天数表示一个以前的日期。
//D1 -= day
Date& operator-=(int day)
{
_day -= day;
//这里需要注意条件,因为天数和月份都没有0天0月
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay();
}
return *this;
}
-
operator-
这里的实现类似与上面的operator+的实现
//D1 - day
Date operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
日期减去日期表示天数 ,注意循环条件的判断,把大的日期往小的日期上靠,当年月都相同时,天数相减就是两个时期之间的天数。
//D1 - D2
int operator-( Date& d)
{
//大的日期减小的日期才有意义
//默认D1 > D2,如果不是,就交换对象
if(*this < d)
{
swap(*this, d);
}
if (!(*this < d))
{
//注意这里的循环条件,
while (_year > d._year ||_year == d._year && _month != d._month)
{
while (_year > d._year || _month != d._month)
{
--_month;
if (_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay();
}
}
return _day - d._day;
}
}
-
operator++(前置++与后置++)
日期增加一天,前置加加。
//前置++
//引用传返回值,减少拷贝提高效率
Date& operator++()
{
*this += 1;
return *this;
}
//后置++
//这里不用引用传返回值,因为tmp是临时对象,出作用域会被销毁,如果用引用传返回值
//就会是随机值(简单理解,出作用域对象还存在就用引用返回,否则传值返回)
Date operator++(int) // 这里的int,只是为了与上面前置++构成重载,没有实际意义
{
Date tmp(*this); // tmp是通过*thid创建的一个临时对象
*this += 1;
return tmp;
}
-
operator--(前置--与后置--)
有++就有--,与上面的++实现类似,就不过多赘述了。
//--D1
Date& operator--()
{
*this -= 1;
return *this;
}
//D1--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
-
存在的问题
到这里就基本完成了一个Date类的全部。但是还存在一个问题,那就是我们在初始化一个对象传参时,完全可能一开始初始的日期就是不合法的,因此,需要对一开始的日期是否合法进行处理。
//检查日期是否合法,合法返真,否则返回假
bool CheckNonLegal()
{
if (_year < 0 || _month >12 || _month < 1 || _day < 1 || _day > GetMonthDay())
{
return true;
}
return false;
}
如果一开始时初始化日期就不合法,那么直接退出程序。因此,我们需要对构造函数再进一步处理:
Date(int year = 0,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
if (CheckNonLegal())
{
cout << "初始化日期不合法" << endl;
exit(-1);
}
}
//初始化成员列表,对私有变量的初始化
Date(int year = 0,int month = 1,int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
if (CheckNonLegal())
{
cout << "初始化日期不合法" << endl;
exit(-1);
}
}