文章目录
自学网站
推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网 风趣幽默的学人工智能:人工智能学习
写在前面
上一节我们学习了构造函数、析构函数、拷贝构造和赋值重载,今天我们就要好好运用前面学习的知识来完成日期类的实现。
日期类的实现
下面先把Date类基本的成员补充完整:
class Date
{
public:
//全缺省的构造函数
Date(int year = 2022, int month = 9, int day = 7)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
//赋值重载
//对于赋值重载函数需要注意的是连续赋值的情况
//至于返回值是Date还是Date&由返回对象在函数结束后是否销毁决定
//这里因为返回的是*this,所以可以使用引用
//d1 = d2 = d3
Date& operator=(const Date& d)
{
if(*this != d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
}
OK,是不是很简单,下面也不难哦。
// ==运算符重载
bool operator==(const Date& d)
{
return (_year == d._year && _month == d._month && _day == d._day);
}
// >运算符重载
bool operator>(const Date& d)
{
if((_year > d._year
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day))
return true;
else
return false;
}
// >=运算符重载
bool operator>=(const Date& d)
{
//可直接复用写好的 ==运算符和 >运算符
return (*this == d) || (*this > d);
}
// <运算符重载
bool operator<(const Date& d)
{
//可直接复用写好的>=运算符
return !(*this >= d);
}
// <=运算符重载
bool operator<=(const Date& d)
{
//可直接复用写好的 <运算符和 ==运算符
return (*this < d) || (*this == d);
}
上面这部分代码,主要是想让大家体会代码的复用性。
我们知道还有++,-- 这类的运算符,它们还分为前置和后置,因为函数名相同,那么该如何区分呢?
// 如若要区分前置和后置,需构成重载
// 规定:如果是后置,需要多加一个参数,这个形参传什么都可以,仅为了区分
// 如:Date operator++();//前置
// Date opeartor++(int);//后置
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)//形参写成int i也可以,无所谓
{
Date ret(*this);
*this += 1;
return ret;
}
// 这里需要格外注意:为什么前置++返回值是Date&,而后置++返回值是Date?
// 我们知道返回值为Date,会调用拷贝构造,这样会影响效率,而Date&却不用。
// 那为什么后置++不用引用返回呢?
// 因为后置++是先赋值再++,返回的是++前的值,所以首先拷贝构造一个ret,返回的是ret的值
// 因为ret是局部对象,所以不能传引用返回。
好,既然前置++和后置++已经给出了,那么前置 --和后置-- 就很容易啦。
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// 后置--
Date operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
好的,上面的内容都很简单,下面我们稍微加大一点难度哦。
为了实现日期±天数,首先实现这两个函数:
// 判断闰年
bool isLeapYear(int year)
{
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
//获取每个月的天数
int GetMonthDay(int year, int month)
{
int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if(isLeapYear(year))
MonthDay[2] = 29;
return MonthDay[month];
}
// 因为GetMonthDay函数可能会被频繁调用,每次都开数组影响效率,所以这部分代码还可以优化一下:
static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//静态成员只会初始化一次
if(isLeapYear(year))
MonthDay[2] = 29;
return MonthDay[month];
//注意哦,这里我给大家挖了一个坑,上面的写法有一个bug,你发现了吗?
//MonthDay[2] = 29;会修改数组里的数据,导致MonthDay[2]变成29。你可以调试去感受。
//综上,下面的代码才是最完美的。
int GetMonthDay(int year, int month)
{
const static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if(isLeapYear(year) && month == 2)
return 29;
else
return MonthDay[month];
}
// 日期+=天数
// d1 += 2;
Date& operator+=(int day)
{
//注意day是负数时的情况
if (day < 0)
return *this -= -day;
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
// 日期+天数
// d1 + 2
Date operator+(int day)
{
//Date ret(*this);
//ret._day += day;
//while (ret._day > GetMonthDay(ret._year, ret._month))
//{
// ret._day -= GetMonthDay(ret._year, ret._month);
// ret._month++;
// if (ret._month == 13)
// {
// ++ret._year;
// ret._month = 1;
// }
//}
//return ret;
//可以用复用+=
Date ret(*this);
ret += day;
return ret;
}
这里又有一个问题了:+复用+=,+=复用+,哪个更优呢?
首先我们知道实现+运算符本身就要调用两次拷贝构造,而实现+=本身没有拷贝构造。如果是+=复用+的话,有4次拷贝构造(+=是用+实现的嘛,所以调用+=本身就有两次拷贝构造,然而实现部分的+又要调用两次拷贝构造,拢共4次);如果是+复用+=的话,只有2次拷贝构造。(日期-=天数同理)
所以说,以后我们只需要实现+=,-=,其他的直接复用它们即可。
// 日期-=天数
// d1 -= 2;
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 - 2
Date operator-(int day)
{
Date ret = *this;
ret -= day;
return ret;
}
在计算日期±天数的时候,大家先在草稿纸上面计算哦,想好了再写代码。OK,下面讲解最后一部分,日期-日期,下面给出的方法挺妙的哦,不信你看看:
// 日期-日期
int operator-(const Date& d)
{
//假设较大值是*this
Date max = *this;
Date min = d;
int flag = 1;
int count = 0;
if(max < min)
{
max = d;
min = *this;
flag = -1;
}
while(min != max)
{
min++;
count++;
}
return flag * count;
}
cin和cout重载问题
哈哈,你不会真的以为没有了吧,怎么会呢,还有补充部分。
对日期类的补充:
上面我们已经对日期类的大部分功能实现完毕了,但是如果直接执行这段代码还是存在问题:
void TestDate()
{
Date d1;
//对于自定义类型的对象流提取操作符>>和流插入操作符<<需要自己实现
cin >> d1;// >>是流提取操作符
cout << d1;// <<是流插入操作符
}
注意哦:
cin 是istream类型的对象,cout是ostream类型的对象,它们都是iostream头文件里面全局属性的对象。
int i = 1;
double d = 2.2;
cout << i << d << endl;
从上面的代码中我们可以看到,相比于C语言,不需要%d,%lf这些指定的输入输出格式,这是为什么呢?其实又是函数重载,C++库里面已经给了内置类型的<<,>>也一样。
所以这里说明了什么问题呢,说明了自定义类型的流插入和流提取需要我们自己实现。
如果我们在类中实现:
class Date
{
public:
...
//成员函数里的第一个形参是隐藏的this指针
void operator<<(ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
如果像上面这样,将operator<<重载到类中实现,那么调用的时候会变成下面这样:
Date d1(2022, 9, 29);
d1.operator<<(cout);// 即d1 << cout;
// 这样是不是很别扭,但是没办法,成员函数的第一个参数默认是隐藏的this指针
因为将<<运算符重载成成员函数后,第一个参数默认是隐藏的this指针,所以我们调用的时候会很怪,这个时候需要将cout 调成第一个参数才可以,也就是说不能把<<重载成成员函数,那我们该怎么办呢?
可以实现成全局函数:
void operator<<(ostream& out, const Date& d)
{
// 如果重载成全局函数的话,还有一个问题就是不能访问类中的私有成员
out << d._year << " " << d._month << " " << d._day << endl;//报错
}
class Date
{
public:
...
private:
int _year;
int _month;
int _day;
};
解决将<<运算符重载成全局函数,不能访问类中私有成员的问题,有两种解决方法:
方法一:
这种方法Java语言喜欢使用,就是在类中定义GetYear(), SetYear()…这些public成员函数,以便在类外进行读写类中私有成员,但是这样实现的话,跟将私有成员变成公有成员的差距不大了,因为这个时候既能读私有成员,又能写成员变量,影响封装,所以不好,C++不推荐这种写法;
方法二:
提供友元,就是在类中声明这个全局变量是该类的友元函数(我是你的朋友,我就可以访问你的私有了),具体关于友元的细节将在下一节进行讲解。
void operator<<(ostream& out, const Date& d)
{
// 如果重载成全局函数的话,还有一个问题就是不能访问类中的私有成员
out << d._year << " " << d._month << " " << d._day << endl;//报错
}
class Date
{
//友元函数
void operator<<(ostream& out, const Date& d);
public:
...
private:
int _year;
int _month;
int _day;
};
代码像上面这样实现之后,就可以正常使用了:
Date d1(2022, 9, 29);
cout << d1;
但是还存在一个问题就是:
这样就编译不通过了,原因是没有给<<运算符重载提供返回值,导致其不支持连续赋值,所以我们需要给函数提供一个返回值,以便再去做下一次流插入的左操作数,具体实现如下:
//流插入操作符:<<
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
这样实现之后,<<就可以正常使用啦,当然了,流提取操作符>>原理同上:
//流提取操作符:>>
istream& operator>>(istream& in, Date& d)//注意哦,此时不能加上const,因为d需要修改
{
in >> d._year >> d._month >> d._day;
return in;
}
const成员函数
我们将const修饰类的成员函数叫做const成员函数,const修饰类成员函数,实际上是修饰该成员函数隐藏的this指针,表明在该函数中不能对类的任何成员进行修改操作。
//Date.h文件
class Date
{
public:
//...
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//test.cpp文件
#include"Date.h"
void Func(const Date& d)
{
d.Print();
}
int main()
{
Date d1(2022,9,29);
d1.Print();
Func(d1);
return 0;
}
如果我们直接这样编译的话是编译不通过的,错误显示如下:
咦,这个报错是什么鬼?
我们知道执行这一行代码的时候,编译器是这样处理的:
d1.Print(); //d1.Print(&d1);
//由于d1是Date类型,所以&d1是Date*
这样的话,Print()成员函数第一个隐藏的this指针类型也就是这样的:
//Date* this,由于this的指向不能发生改变,
//所以编译器会将成员函数的第一个隐藏的this指针处理成Date* const this;
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
可能有老铁会说:对呀,这个你说的,我都理解呀,那为什么会编译不通过呢?
其实呀,如果仅执行 d1.Print();是不会报错的,错就错在 Func(d1); 这个函数体里面调用的Print()。为什么这样说呢,请继续看:
#include"Date.h"
void Func(const Date& d)
{
d.Print();//d.Print(&d);
//这里的&d类型是const Date*
}
//而我们知道成员函数Print()第一个隐藏的this指针类型是Date*
//将const Date* 传递给 Date* const(本质还是Date*)属于权限放大,这个是不允许的,所以编译报错
//只允许权限不变或者权限缩小
//d1.Print();//即d1.Print(&d1);Date*传递给Date*属于权限不变
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
那么我们该如何修改呢?我们知道如果要解决Func()函数里面的错误的话,这需要将成员函数Print()里面的第一个隐藏的this指针类型改成const Date* const即可,但是由于它是隐藏的,不能显示修改,那我们该怎么办呢?不要担心,很简单,像下面这样即可:
//在函数后面加上const,相当于在第一个参数(也就是隐藏的this指针)的前面加上const
//这个时候this指针的类型就变成了const Date* const类型
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
在成员函数的后面加上const之后,this类型就变成了const Date* const类型,属于权限很低的类型,这样的好处是不管传递过来的变量类型是Date*(权限缩小),还是const Date*(权限不变)编译都能通过,不会存在权限放大这么一说了。
可能有老铁会问了,既然给成员函数后面加上const之后可以有效避免权限放大的问题,那么我们为什么不在每个成员函数的后面都加上一个呢?
我要说的是,加上const之后,this指针变成const Date* const类型,所以此时this指向不能修改,this指向的内容同样也不能修改,所以说只有成员变量不用修改的成员函数才可以加上const。比如日期类里面的++, --不可以加,而+, -可以。
好的,有了前面的基础,请看下面几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他的非const成员函数吗?
- 非const成员函数内可以调用其他的const成员函数吗?
这就很简单啦,问题1不可以,权限放大;问题2可以,权限缩小;问题3不可以,权限放大;问题4可以,权限缩小。
大厂面试真题
1.已知表达式++a中的"++"是作为成员函数重载的运算符,则与++a等效的运算符函数调用形式为( )
A.a.operator++()
B.a.operator++(0)
C.a.operator++(int)
D.operator++(a,0)
解析:这道题很简单,前置++嘛
2.在重载一个运算符为成员函数时,其参数表中没有任何参数,这说明该运算符是( )
A.无操作数的运算符
B.二元运算符
C.前缀一元运算符
D.后缀一元运算符
解析:重载为成员函数时,其函数的参数个数与真实的函数参数个数会减少1个,减少的则通过this指针进行传递,所以无参则说明有一个参数,然后对比前置后置++,就很容易解决本题啦。
3.哪个操作符不能被重载 ( )
A.*
B.()
C… (点)
D.[]
E.->
解析:不能被重载的运算符只有5个, 点号. 三目运算?: 作用域访 问符:: 运算符sizeof 以及.*
4.下列关于赋值运算符“=”重载的叙述中,正确的是( )
A.赋值运算符只能作为类的成员函数重载
B.默认的赋值运算符实现了“深层复制”功能
C.重载的赋值运算符函数有两个本类对象作为形参
D.如果己经定义了复制拷贝构造函数,就不能重载赋值运算符
解析:
A. 赋值运算符在类中不显式实现时,编译器会生成一份默认的,此时用户在类外再将赋值运算符重载为全局的,就和编译器生成的默认赋值运算符冲突了,故赋值运算符只能重载成成员函数;
B.默认的赋值运算符是按成员赋值,属于浅赋值;
C.参数只有一个,另一个通过this指针传递;
D.两个函数的调用场景不同,相互没有影响