在前面学过默认成员函数后,我们就可以写一个简单的日期类了。
如何写呢?我们可以先分析分析。
日期类的成员变量都是int类型,那么构造函数是要显式定义的,成员变量都是int类型,因此浅拷贝即可。
因此拷贝构造、析构、赋值操作符重载都不需要我们显式定义,使用编译器生成的就好。
#includeusing namespace std; int MonthDay[13] = { 0, 31,28,31,30,31,30,31,31,30,31,30,31 }; bool IsLeapYear(int year) { if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) return true; else return false; } class Date { public: int GetDay(int year, int month)const { if (IsLeapYear(year) && month == 2) return 29; else return MonthDay[month]; } Date(int year = 1, int month = 1, int day = 1) :_year(year), _month(month), _day(day) { if (year <= 0 || (month < 0 || month >= 13) || day > GetDay(year, month)) { cout << "日期错误" << endl; cout << year << "-" << month << "-" << day << endl; exit(-1); } } void swap(Date& d) { ::swap(_year, d._year); ::swap(_month, d._month); ::swap(_day, d._day); } Date& operator+=(int day) { if (day < 0) { return *this -= -day; } _day += day; while (_day > GetDay(_year, _month)) { _day -= GetDay(_year, _month); _month++; if (_month > 12) { _month = 1; _year++; } } return *this; } Date operator+(int day) { return Date(*this) += day; } Date& operator-=(int day) { if (day < 0) { return *this += -day; } _day -= day; while (_day < 1) { _month--; if (_month < 1) { _year--; _month = 12; } _day += GetDay(_year, _month); } return *this; } Date operator-(int day) { return Date(*this) -= day; } bool operator>(const Date& d)const { 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)const { return *this > d || *this == d; } bool operator==(const Date& d)const { return _year == d._year && _month == d._month && _day == d._day; } bool operator!=(const Date& d)const { return !(*this == d); } bool operator<(const Date& d)const { return !(*this >= d); } bool operator<=(const Date& d)const { return *this < d || *this == d; } Date& operator++() { *this += 1; return *this; } Date operator++(int) { Date ret(*this); *this += 1; return ret; } Date& operator--() { *this -= 1; return *this; } Date operator--(int) { Date ret(*this); *this -= 1; return ret; } int operator-(const Date& d)const { Date d1(*this); Date d2(d); int flag = -1; int ret = 0; int num1 = 0; int num2 = 0; if (d1 > d2) { d1.swap(d2); flag = 1; } for (int i = 1; i < d1._month; i++) { num1 += GetDay(d1._year, i); } num1 += d1._day; for (int i = 1; i < d2._month; i++) { num2 += GetDay(d2._year, i); } num2 += d2._day; for (int i = d1._year; i < d2._year; i++) { ret += 365; if (IsLeapYear(i)) ret++; } ret = ret + num2 - num1; return ret * flag; } ostream& operator<<(ostream& out,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; } private: int _year; int _month; int _day; };
代码不难,但可能会遇到一些细节处理上的问题。
一些问题前置++与后置++的重载
C++中如何区分两者呢,两者的函数名(操作符)作用对象(形参)都是一样的;
因此C++规定了让后置++多一个int参数,用于与前置++构成重载得以区分。
Date& operator++();前置++
Date operator++(int);后置++
所以调用后置++操作符重载时,会多传一个int值,用于区分前置++,这个参数不需要我们显式去传,编译器帮我们处理。
但注意后置++的操作符重载不能写成Date operator++(int a = 0);,这是不被编译器所允许的。
因为这样会导致调用不明确,我们调用前置++重载时是该调用无参的还是缺省呢?这里就会产生歧义,因此不能给后置++重载的int参数缺省值。
临时变量与const的冲突cout << d1++ << endl;这段代码实际上就会出错了,会报错:
二元“<<”: 没有找到接受“Date”类型的右操作数的运算符(或没有可接受的转换)
说明上述代码没有找到对应的函数,可是明明参数什么的都是一致的啊?
d1++调用的是d1.operator++(int)函数,而此函数返回值是一个传值返回,而传递的过程实际上是在调用函数栈帧中创建了一个临时变量来接收被调函数的返回值,因此d1++是一个临时变量,具有常性,但我们的ostream& operator<<(ostream& out,Date& d)函数的参数类型没有被const修饰,而C++对const的对象和非const对象区分得很严格,它们会被当做不同的类型,因此报错。
cout << ++d1 << endl;会不会报错呢?这里返回的是d1的引用,而不是一个临时变量的拷贝,对++d1的访问实际上就是对d1的访问,因此不会报错。
流提取&流插入我们没有将这两个函数写成成员函数,而是设置为友元函数。
其实可以是可以,如下:
ostream& operator<<(ostream& out)const
{
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
istream& operator>>(istream& in)
{
in >> _year >> _month >> _day;
return in;
}
设置为成员函数,那么第一个参数就是隐含的this指针,第二个参数是输入输出流的对象。
调用方式:d1 << cout;
调用时对象就只能作为左操作数,cout或者cin只能作为右操作数,与我们的使用习惯不符合,因此这里采用友元的方式处理流插入和流提取。
声明和定义分离首先我们要知道全局函数我们是不能放在头文件中的,因为头文件中得函数定义会在各个源文件展开并编译,同一个函数在两个不同的源文件有了不同的地址产生冲突,造成重定义。
如果想要成员函数的声明和定义分离,那成员函数的定义也一定得放在.cpp文件中而不是头文件,原因同上,同一个函数在多个源文件内被编译,造成重定义。
那为什么函数定义在类中,能正常编译呢?
在类中定义得函数是默认内联的,不会放入符号表,即使头文件在多个源文件展开,也不会有重定义错误,因为它们都单独作用于所存在的源文件(没有外部链接属性)。
那可能又会问,放在类的函数不一定会被编译器当做内联,那这些没有被当做内联的函数会有函数地址,为什么也不会有重定义错误?
答案是即使没有被编译器当做内联,即不会在调用的地方被展开,但是其是具有内联属性的,而无法被外部链接,就不会发生多个源文件相同函数有冲突的情况。
例如下面这种:
//func.h
inline void print(const int a);
//func.cpp
#include"date.h"
inline void Test(const int a)//编译器不会采用我们的建议使其内联
{
for(int i = 0; i < 100; i++)
{
cout << a << endl;
}
}
//main.cpp
#include"date.h"
int main()
{
const int a = 1;
Test(a);
return 0;
}
链接错误:LNK2019: 无法解析的外部符号 “void __cdecl print(int)” (?print@@YAXH@Z),函数 main 中引用了该符号
则表明在main.cpp中找不到func.cpp中的func函数,可以这样理解——func函数虽然有地址,但是由于我们的inline声明,它无论如何都有内联属性,无法被外部链接。
结论:
- 公有的函数(包括友元)都不能定义在头文件,否则会有函数重定义的错误;
- 类的成员函数如果不是内联函数,也不能定义在头文件,否则会有函数重定义的错误;
- 若函数声明了是内联的,编译器也采纳了,那么内联函数没有地址,无法被外部链接;
- 若函数声明了是内联的,但编译器没有采纳,虽然产生了函数地址,但有内联属性,也无法被外部链接;
浓缩成一句话,被声明为内联的函数,即使编译器没有采纳内联意见,但仍保留内联属性(没有外部连接属性),必须没有外部链接属性的函数才能定义在头文件中。
说到外部链接属性,那么static修饰全局函数也是改变其连接属性,让函数只能在本源文件内被调用(无法被外部链接)即使其他源文件有一个完全相同的函数定义,但我链接此函数时只会在本源文件找,就不会有函数地址的冲突,因此static修饰全局函数时,这个函数也可以定义在头文件中而不发生重定义的错误。
当然也可以说它们没有了外部链接属性,就不会被放入符号表,也不会有符号表的合并中同一个函数却有多个地址的冲突,因此多个源文件链接到一起时不会报错。(我猜这个是主要原因)
关于代码复用我最开始觉得代码复用很不好,因为一个函数的函数体内要调用另一个函数来实现部分功能,这就需要有压栈的开销,这不是浪费资源吗?因此我带着怀疑在日期类的+运算符重载的重载中复用了+=运算符的重载。
但是当我做修改和添加功能时,我才发现代码复用的好处:
这降低了我们维护代码的难度,相似但细节上有差异的函数我们不需要一个地方出问题了就去全盘修改,而是只需要修改被复用了的函数,最重要的是我们省事啊!



