c++类(中)续
这一章我们要完成的任务有:
一、日期类
(1)什么是日期类?
首先日期类就类似于网络上的日期计算器,那我们为什么要完成这个东西呢?
这是因为日期类中需要运用很多我们前面c++类的知识,可以更好的帮助我们理解c++类。
我们需要完成类似的工作。
(2)日期类的初始化与销毁
我们要完成日期类的初始化与销毁,那初始化是不是需要我们的构造函数,销毁需要我们的析构函数,那日期类需要初始化无非是年月日,我们需要构造函数来满足我们的初始化操作,但是我们需要析构函数吗,上一章我们学过如果没有申请空间那么我们就可以使用默认的析构函数即可。
所以在下面我们只需要完成构造函数即可。
(3)我们需要完成的操作
这里我们采用申明与定义分离的方式。
.h文件里
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1);
void print();
bool operator<(Date& d);
bool operator<=(Date& d);
bool operator>(Date& d);
bool operator>=(Date& d);
bool operator==(Date& d);
bool operator!=(Date& d);
//比较两日期
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
//日期加减天数
int operator-(Date& d);
//两日期之间隔的天数,会自动选出大的日期与小的日期,然后相减
Date operator++(int);//后置++
Date operator++();//前置++
//流运算符重载
void operator<<(ostream* out);//有问题
void operator>>(istream* in);//有问题
private:
int _year;//年
int _month;//月
int _day;//日
};
大家可以看到最后两个函数是有问题的大家可以思考为什么有问题,这两个函数我会留到最后解决。
(1)初始化与输出日期
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::print()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
这一部分很简单,大家看看就行了。
(2)日期加等于天数
首先我们需要明白日期加天数有两种情况:
如果是前者那就好办许多,如果是后者的话我们这里使用进位的方式,
什么意思?
就是当相加后的天数大于当前月的天数,我们就减去当前月的天数,然后月份加加,当我们的月加到13我们就将年加加,月置为1,这样就可以完美的解决问题,但是怎么获取当前月的天数呢?所以我们需要一个函数来获取当前月的天数
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1);
void print();
bool operator<(Date& d);
bool operator<=(Date& d);
bool operator>(Date& d);
bool operator>=(Date& d);
bool operator==(Date& d);
bool operator!=(Date& d);
//比较两日期
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
//日期加减天数
int operator-(Date& d);
//两日期之间隔的天数,会自动选出大的日期与小的日期,然后相减
Date operator++(int);//后置++
Date operator++();//前置++
//流运算符重载
void operator<<(ostream& out);//有问题
void operator>>(istream& in);//有问题
int GetMonthDay(int year, int month)
{
int MonthDay[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//第一个位置置0或者负数,因为我们
//是通过月份来作为数组下标来访问我们的天数,而0月是不合法的
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 29;
else
return MonthDay[month];
}
private:
int _year;
int _month;
int _day;
};
大家可以看到我们将这个函数定义在类里,前面我们学过定义在类里的函数默认内联,内联函数一般运用与代码量少且频繁调用,这里获取天数的函数大家想一想是不是非常频繁啊。
然后我们再看代码部分,这里我们采用数组的形式来返回天数,大家如果想不到的话也可以用if,else if嵌套或者switch case语句,但是论简单的话还是数组比较简单,最后看我们如何判断闰年的2月与平年的2月的,只有闰年的2月才返回29其他都直接返回month下标的数组元素即可,此外,返回29这里有些同学想着更改数组元素,但我们下次再调用的时候这里的2月都被更改为29了。
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;//this指针隐藏,但在函数里我们可以使用
}
这就是我们的代码了。
然后我们再看我们的逻辑,我们是不是改变了我们原本的日期啊,那日期加天数是不是不能改变目前的日期,那我们就写不改变的加。
(3)日期加天数
这里我们是可以偷懒的,上面我们不是写了加等吗,那我们创建一个新的对象然后用新的对象加等,这就是我们的加的逻辑。
偷懒版:
Date Date::operator+(int day)
{
Date d = *this;
d += day;
return d;
}
那有些同学非要自己写该怎么写呢?
Date Date::operator+(int day)
{
Date t = *this;
t._day += day;
while (t._day > GetMonthDay(t._year, t._month))
{
t._day = t._day - GetMonthDay(t._year, t._month);
t._month++;
if (t._month == 13)
{
t._year++;
t._month = 1;
}
}
return t;
}
完成这些以后,细心的同学可能会发现我们的加返回并没有用引用,这是因为我们在这个函数里创建的新的对象,出了这个函数这个对象就销毁了,那如果我们传引用就会造成我们的野引用。
此外我们再了解一个知识点,我们上面用的的+复用+=等,那我们可不可以用+=复用+?并且哪里效率高?
这里我们直接给答案可以,并且 +复用+=效率高。
**红色圈起来的部分就是我们调用拷贝构造的地方,大家可以很明显的看到右边明显多了几次,准确的来说多了3次。**这里效率的差别全在拷贝构造。
(4)日期减等天数与减天数
减等与减之间的关系与加与加等的关系类似,所以我在这里只给出代码,大家可以对比来理解。
Date& Date::operator-=(int day)
{
_day -= day;
while (_day < 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date d = *this;
d -= day;
return d;
}
(4)两日期之间的关系
两个日期之间的关系有>,>=,<,<=, ==, !=, 这里我们其实只需要完成两个即可。
为什么?例如我们完成 > 与 等于,那<不就是!>=,<=就是!>,这样来复用就都可以完成。
<:
bool Date::operator<(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==(Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
当我们完成上面的我们就可以复用于其他函数了
bool Date::operator<(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<=(Date& d)
{
return (*this == d) || (*this < d);
}
bool Date::operator>(Date& d)
{
return !(*this <= d);
}
bool Date::operator>=(Date& d)
{
return !(*this < d);
}
bool Date::operator==(Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool Date::operator!=(Date& d)
{
return !(*this == d);
}
(5)日期前置++与后置++
前置++是先++再使用,后置++是先使用再++
Date Date::operator++(int)
{
Date d = *this;
*this += 1;
return d;
//后置++是先使用原本的值再++
// int a=0,x=1;
//例如int x+=a++;,执行完x=1,a=1,这里逻辑一样。
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
(6)两日期相减
两日期相减我们有两种思路
一、
二、
这里我们只说第二种,第二种什么意思呢?
我们上面不是完成了日期++吗,那我们让小的日期一直++直到等于大的日期,那++的次数不就是相差的天数,有的同学就会问这样效率会不会很低,大家要知道我们现在的cpu计算能力是上亿次的,而我们的日期之前的差值无非就几千上万天,加上拷贝构造对于cpu来说也是小菜一碟。
代码:
int Date::operator-(Date& d)
{
Date Max = *this > d ? *this : d;//选出大的日期
Date Min = *this > d ? d : *this;//选出小的日期
int count = 0;//记录++次数也就是天数
while (Min != Max)
{
Min++;
count++;
}
return count;
}
完成了上面的我们的日期类就差不多了。
(7)固定化格式输出输入日期
上面我们的日期类都是给定值然后再计算,那我们能不能自己输入日期呢和输出日期就是我们需要完成的,我们将这种输出输入叫做以固定格式输入与输出,在c语言中sacnf与printf就是这种方式
其实上面在.h文件中我也提到过:
void operator<<(ostream& out);//有问题
void operator>>(istream& in);//有问题
ostream 与 istream 是我们的标准输入输出流,大家要是不了解可以去看一下我写的文件这一节。
但这里的问题不是不能输出与输入,而是不符合语法。
我们现在写一下:
void Date::operator<<(ostream& out)//有问题
{
out << _year << ':' << _month << ':' << _day << endl;
}
void Date::operator>>(istream& in)//有问题
{
in >> _year >> _month >> _day;
}
为什么我说它有问题呢我们可以看:
大家可以看到是能输出的,但是它的写法不符合我们的习惯,所以我们就pass掉
那大家知道为什么会出现这种情况吗?在上一节c++中我们不是说过c类里的函数隐含有一个this指针,那我们的>><<就是有两个操作数,而我们上一节又说了左操作数会默认在操作符的左边,右操作数在操作符的右边,故此导致这种情况!!!
所以我们只能放在类外来实现,但是类外无法访问我们的类的类部成员,这怎么办?这里有两种解决办法,第一种使用get函数。
例如:
class point
{
int getx()
{
return x;
}
private:
int x;
};
这里我就不多写了,大家自己可以尝试完成。
第二种方式将其定义为友元函数。
友元函数在下一章,这里大家只需要知道就行,下一章详细来讲。
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1);
void print();
bool operator<(Date& d);
bool operator<=(Date& d);
bool operator>(Date& d);
bool operator>=(Date& d);
bool operator==(Date& d);
bool operator!=(Date& d);
//比较两日期
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
//日期加减天数
int operator-(Date& d);
//两日期之间隔的天数,会自动选出大的日期与小的日期,然后相减
Date operator++(int);//后置++
Date& operator++();//前置++
//流运算符重载
//void operator<<(ostream& out);//有问题
//void operator>>(istream& in);//有问题
int GetMonthDay(int year, int month)
{
int MonthDay[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//第一个位置置0或者负数,因为我们
//是通过月份来作为数组下标来访问我们的天数,而0月是不合法的
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
return 29;
else
return MonthDay[month];
}
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);
写法跟上面有问题的写法一样的
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << ':' << ':' << d._month << ':' << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
最后我们来说明一下为什么我们的返回值是ostream与istream,我们之前内置类型是不是可以连续输入与输出,其实质就是从左到右连续输入与输出。
以上就是日期类的全部内容,这里涉及到了许多类的知识,希望大家能够全部理解。
二、取地址运算符重载
(一)const成员函数
1、将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。
2、const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 constDate* const this
这里是很标准的权限放大,实际下面是Dateconst this 但是这里的const修饰的本身,上面是禁止修改我们的对象,下面是禁止修改指针,怎么解决呢?在下面的Date前加const
祖师爷为了解决这个问题就在函数的参数列表后加const, 如上图所示。
//本质print(const Date*const this)
void Date::print()const
{
cout << _year << ':' << _month << ':' << _day << endl;
}
如果我们的非const对象调用,这里属于权限缩小,这里是可以调用的。
总结:不修改成员变量的函数都可以加const。
(二)取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。
Date* operator&()
{
return nullptr;//这里我不想别人取到对象地址
}
const Date* operator&()const
{
return this;//这里一定返回对象地址
}
没有实际意义,只是作为了解。