目录
我们用前面学的知识简单实现一下规范点的日期类吧,定义两个.cpp文件,用于测试和函数定义,一个.h文件用于头文件包含。
判断日期合法
从我们测试结果来看,输入了一个非法的日期却没有检测,所以我们要判断日期的合法性
之前我们写了一个获取天数的函数
int Date::GetMonthDay(int year, int month) const
{
assert(month > 0 && month < 13);//合法
int monthArray[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 monthArray[month];
}
}
再完善构造函数
Date::Date(int year, int month, int day)
{ //assert
if (month > 0 && month < 13 && day <= GetMonthDay(year, month) && day>0)//这里只对月日做判断
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
exit(-1);
}
}
日期运算
+=
理论来说,+=天数可以不用写 返回值,(this指针直接改变了它的值),
从运算符的特性来说(不支持d1 +=d2 += d3,但支持d1 = d2 + d3),要支持连续赋值,就得添加返回值。
Date& Date:: operator+=(int x)//域名指定的是名字而不包含返回值
{
_day += x;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
if (++_month > 12)
{
_year++;
_month == 1;
}
}
return *this;//连续赋值
}
+运算
+运算不能改变原对象,所以需要用到拷贝构造(赋值重载),而且之前函数重载那一篇文章提到过代码的复用,为我们减轻实现压力。
Date Date:: operator+(int x)
{
Date tmp(*this);
tmp += x;
return tmp;
}
注意这里tmp是临时变量不能引用返回。有人说可以将tmp变成静态的,然后再用引用返回减少拷贝,但这样就陷入了另一个误区
通过调试发现,下次调用这个函数时,这个静态对象还保留着之前的值,就会出现这样的错误!
这里我们是用+来复用+=,那反过来怎么用+=来复用+呢?
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 x)
{
*this = *this + x;//默认赋值重载
return *this;
}
大家觉得哪种方式更好呢?当然是第一种了。通过对比发现,第一种只在创建临时对象时调用了一次拷贝构造,第二种有三次拷贝,复用有两次,赋值又有一次拷贝构造,所以推荐用第一种方式。
- = && -
与刚才类似,需要注意退位,如果天数不足,前面我们是减去多余的天数,那这次我们就加上上一月的天数。
Date& Date:: operator-=(int x)
{
_day -= x;
while(_day <= 0)
{
if (--_month < 1)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
}
Date& Date:: operator-(int x)
{
Date tmp(*this);
tmp -= x;
return tmp;
}
题外话:如果我想+=一个负数怎么做呢,这种情况也是合理的啊?利用我们刚才写好的-=重载,我们只需在+=的后面加上一个判断并复用就可以了,注意我们这里输入的是负数,所以要加上负号才能正确使用。(负号不一定代表是负数)
if (x < 0)
{
*this -= -x;
return *this;
}
测试代码中发现一些小的问题,比如没有进入 -=说明是因为将*this -= -x写成了_day -= -x,造成不匹配,还有输出日期不对,是因为判断应该放在_day -= x这句代码之前执行,希望引以为鉴。
前置++/--与后置++/--
虽然+=和-=可以取代++,--的工作,但继承了c语言的特性,c++对自定义类型的前置后置++、--做了个良心的区分。
我们先来看看它们在类中的声明
Date& operator++();//前置
Date& operator++(int);//后置
前置很好理解,而后置函数体的int无实际含义,仅仅是为了占位,你可以理解为 a++0来区分它们。
Date Date:: operator++(int)//不推荐
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& Date:: operator++()
{
*this += 1;
return *this;
}
注意后置++需要先创建临时变量,然后类自增1,最后返回临时变量,需要调用两次拷贝构造所以不推荐使用。
日期 - 日期
相比前面的日期+-一个数,日期减日期更为复杂,它的返回值是一个整形,且需要考虑闰年,闰月问题,这里给大家一种累加法求两个日期相差的天数。
在函数体内涉及一个大小问题,小的要累加直到和大的相同为止,可以设置一个flag开关,用于进行正负计算。
int Date:: operator-(Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n*flag;
}
在运行中发现一个大意的问题揪了半天才揪出来。是这样的,刚开始小的日期计算没问题,但跨日期大了就报错了,而报错位置就是assert的月份出错了(如果不写aseert都检查不出来)。然后从GetMonthDay这个函数找,发现没问题,然后就去找调用了它的运算符重载,因为++是复用+=的,最终在+=函数里发现month的=写成了==,导致一遇到跨年的日期就会报错,而调试也要调试365次才能发现异样,希望大家能从我的这次粗心中学到一点经验,共勉。
日期类流插入重载
上面代码中我们都是调用Print来打印日期,再有的时候测试起来可能不太方便,在学习c++入门时我们简单介绍了cout是流插入函数,而<<是一个运算符重载,意为将右边的数据流入cout对象中,通过运算符重载概念的引入,现在大家对cout可以识别内置类型想必有了更清晰的认识。
int i;
double s;
cout << i;//cout.operator(i)
cout << s;//cout.operator(s)
具体原理就是识别不同类型,调用不同重载函数,这个过程就是 运算符重载 + 函数重载的过程
对于内置类型,c++的库已经为我们提供iostream接口供我们使用,而对于自定义类型,需要我们自己来实现,所以c++为我们提供了流插入函数的声明使我们可以自己编写。
我们可以使用c++的ostream(输出流)来添加cout的自定义类型重载
void Date::operator <<(ostream& out);//out == cout
{
out << _year << "/" << _month << "/" <<_day << endl;
}
但是我们不能像 cout<<d1一样打印出d1,因为在类中运算符重载的左操作数是对象而不可能是cout,我们只能反过来调用或者通过d1.operator<<(ostream& cout)调用。但这样是有问题的,比如不支持链式调用。(d1<<d2<<cout? d1<<cout<<d2?)
在全局定义是不是就可以了呢?
我们可以将它放到全局,但全局是无法访问私有成员变量的,目前有两种方法可以访问成员变量,
1.变成公有(不推荐,破坏封装性)
2.类中实现相应的获取成员变量的函数
现在我们引入一种c++中新的方法:友元函数,使之能访问类中的private成员变量
为了实现运算符链式调用的功能,我们给其添加返回值,具体原理是从左向右依次读取cout和右操作数,然后返回ostream类型(cout),cout再作为左操作数与下一个操作数进行调用。
//类外
ostream& operator<<(ostream& out,const Date d)//out即cout
{
out << _year << "/" << _month << "/" <<_day<<endl;
return out;
}
friend ostream& operator<<(ostream& out, const Date d);//类里
这里传引用返回保证每个运算符都能访问前面运算符输出的流对象。
通过这种类外自定义cout的方式 ,解决了c语言无法用printf进行输出自定义类型的问题
日期类流提取重载
既然支持流插入,那也应该支持流提取
//类里
friend istream& operator>>(istream& in, Date& d);//无const
//类外
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
//assert(d._month > 0 && d._month < 13 && d._day <= d.GetMonthDay(_year, _month) && d._day>0);
return in;
}
注意这里不要输错天数,我本想使用assert检查,但我只对istream进行了友元声明,所以无法识别。
使用内联
像cout和cin这种体积小的函数,多次调用可能会浪费太多时间,我们可以使用内联来提高运行效率
我们在.h文件里直接声明和定义cout和cin函数,并加上inline的前缀,这样在编译过程展开直接就能call到函数的地址,无需再去符号表里查找并节约了函数调用的开销。
类中声明和定义的函数可能会被编译器默认为内联函数(短小精悍的函数),所以我们可以将这样的函数直接在类里面展开。
const类问题
先给大家普及一下被const类型修饰的类型的特性——需要在定义的时候初始化
我们先来举个例子
class A
{
public:
void Print()
{
printf("%d", _a);
}
A(int a)
{
_a = a;
}
private:
int _a ;//缺省
};
int main()
{
const int* a;//int const* a
const int b;
int* const c;
const A aa;
return 0;
}
上述四个对象哪些是不初始化可以被编译的,哪些必须初始化?
答案是除了第一个,都要初始化对象,不是说被const修饰的类型都要初始化吗,那是因为作为指向常量的指针,它所储存的是一个具体的内存地址而不是一个数,所以可以不用初始化。第四个比较特殊,如果你在类声明中定义了构造函数或者给所有成员变量给了缺省值,就算成功实例化。
题外话:引用在声明时必须指向一个对象,这里不做考虑。
弄清楚这个以后,我们都知道类中成员函数都有隐含的this指针,如果我们执行下面的操作就会编译出错。
const A *paa = &aa;
//传递过程,不支持显式写
aa.Print(&aa);
aa.Print(paa);
而在函数体中的this指针为
A* this;
根据规定,在传递参数这个过程中是不允许显式写出其地址和this指针的,为了解决这一权限放大问题,规定在函数体外书写const的形式来修饰this指针,使其变成常指针。
这种场景一般适用与将传递const类型的类对象
//类里
void Print() const//cosnt A * this
{
printf("%d", _a);
}
//类外
void Fun(const A& a)
{
a.Print();
}
所以对照日期类,我们可以给内部不改变成员变量的成员函数加上const,使代码更加安全(友元函数没有this指针所以不用在外加const)
取地址及const取地址操作符重载
前面我们讲了四大成员函数,作为最后的两个成员函数,一般情况下不需要自己写,它们很好理解
题外话:六大成员函数如果自己手动写,必须在类里声明,运算符重载可以在类里可以在类外,函数同样如此,不能因为学了类就固化了思维
自定义取地址重载
可以看到可以通过这种方式改变了地址,但其实只是改变了输出结果,通过调试还是能看到真实地址。注意这里const取地址重载后要加const构成重载