放弃信念,无异死亡。
文章目录
我们规范一点,还是采用分文件的方式来写。
💖1.声明日期类
我们在头文件里声明我们的日期类。对于日期类而言,只要写一个构造函数就够了。因为日期类里全是内置类型,默认的构造函数不对其处理,而默认生成的拷贝构造函数和赋值运算符重载函数已经够用了(值拷贝),也不需要析构函数。在这里运算符重载对我们来说才是真正的重点。
ok,我们先简单开个头:因为要对日期类进行运算,在声明日期类的时候,顺便写一个能够获取某年某月的天数的函数,方便后续进行运算。
Date.h
#pragma once
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
//判断是否为闰年
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
//获取天数
int GetMonthDay(int year, int month);
//构造函数
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
🌟2.构造函数和获取天数
🔥2.1获取天数
int Date::GetMonthDay(int year, int month)
{
assert(year >= 0 && month > 0 && month < 13);
//加static是为防止函数频繁调用每次都开辟数组空间。
const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && isLeapYear(year))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
☀️2.2 构造函数
Date::Date(int year, int month, int day)
{
if (year >= 1 &&
month <= 12 && month >= 1 &&
day >= 1 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法访问" << endl;
}
}
⚡️*3.关系运算符重载
关于运算符重载我们不用全部都实现,对于逻辑上有关联的一对运算符,我们只要实现其中一个就可以了,另一个可以通过逻辑运算符复用写好的那一个。
🌊3.1 < 和 ==
假设我们实现了小于和等于的运算符重载,那我们就可以利用这两个函数去实现其它运算符。这种方式不仅仅适用于日期类,其它的自定义类型需要写运算符重载函数的,都可以按照这种思想去写。
//小于运算符重载
bool Date::operator<(const Date& d)
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && d._day < d._day))
{
return true;
}
else
{
return false;
}
}
//等于运算符重载
// d1 == d2
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
🐮3.2 <=
*this 就是d1,d是d2的别名。这里复用了<和=,有了这个例子,其它的对于大家来说是不是就很简单了。
// d1 <= d2
bool Date::operator<=(const Date &d)
{
return *this < d || *this == d;
}
🐸3.3 >
大于就是<=取反。
bool Data::operator>(const Date& d)
{
return !(*this <= d);
}
🌺3.4 !=
!=就是==取反。
bool Data::operator!=(const Date& d)
{
return !(*this == d);
}
🍀3.5 测试
void TestDate1()
{
Date d1(2022, 5, 18);
Date d2(2023, 3, 20);
Date d3(2023, 3, 20);
cout << (d1 < d2) << endl;
cout << (d1 > d2) << endl;
cout << (d1 == d3) << endl;
cout << (d2 <= d3) << endl;
cout << (d2 == d3) << endl;
}
注意:流插入运算符运算级非常的高,我们为了然日期类对象先进行运算必须加()
。
运行结果:
🌾4. inline优化
我们发现关系运算符重载的代码都挺短的,我们可以把它们写成内联函数进行优化。但是内联函数的声明和定义不能分离, 因为内联函数不会把函数名放进Data.o的符号表,声明在Date.cpp展开后链接不上。
这个时候我们把复用了其他运算符的重载函数直接在类里面定义,连inline都不用加了,因为类里面默认是内联函数。
其实我们都可以把它们放进类里,因为日期类的运算符重载都不太长,都放过去也不太好。而且为了演示分文件的过程,我们把需要自己实现的,在类外面定义。复用的放在类里面。
#pragma once
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
class Date
{
public:
//判断是否为闰年
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
//获取天数
int GetMonthDay(int year, int month);
//构造函数
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator==(const Date& d) const;
bool operator<(const Date& d) const;
// inline不支持声明和定义分别放到.h 和.cpp
// 所以成员函数中要成为inline最好直接在类里面定义
// 类里面定义默认就是inline
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);
}
// d1 <= d2
bool operator<=(const Date& d) const
{
return *this < d || *this == d;
}
private:
int _year;
int _month;
int _day;
};
🌼*5 算术运算符重载
🌙5.1 +
加法运算进位要考虑当月的天数,月份是否合法,此时我们写的获取天数的函数就可以用上了。边界控制好了,就好说了。
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;
}
大家想一想这个代码对不对?管他对不对,我们来测试一下:
// d1 + 100 -->d1.operator+(day);
void TestDate2()
{
Date d1(2022, 5, 18);
Date d2 = d1 + 15;
d2.Print();
d1.Print();
}
运行结果:
因为d1的地址传给了隐含的this指针,_year、_month、_day其实是this->_year、this->month、this->day。上面写的代码一上来就把自己改了,这不是我们所期望的。像i+100是不会变的,i+=100才会变。这个问题很容易解决,我们先拷贝一个。
正确代码:
Date 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;
}
运行结果:
注意:ret出了函数作用域就销毁了,不能用引用做返回值,不然就会出现野指针问题。 可能你用引用做返回值有时候结果还是正确的,这个时候只不过是编译器没有清理栈帧而已,你再调用其他函数试试。
🌏5.2 +=
我们之前写的+的错误代码是不是就是+=的逻辑呀!+=是有返回值的,因为要支持连续的+=。
比如说:d2 += d1 += 100
Date& Date::operator+=(int 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;
}
因为*this就是d1,出了作用域不会销毁,this才会销毁,所以可以用引用返回,减少一次传值返回的拷贝。
测试:
void TestDate3()
{
Date d1(2022, 5, 18);
Date d2 = d1 + 15;
Date d3;
d3 = d1 + 15;
d2.Print();
d1.Print();
d1 += 15;
d1.Print();
}
运行结果:
🎃5.3 +和+=谁复用谁比较好?
+复用+=效率高,这样节省了两次拷贝。
同样算术运算符的重载还是把复用的写进类里,自己实现的在类外定义。
🎉5.4 -=
我们先写-=,等会-复用-=。减需要借位,借位借的是前一月的,且0月不能借位。注意控制好边界。
注意:如果减的是负数,我们就加上它的相反数。
Date& 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;
}
测试:
void TestDate3()
{
Date d3(2022, 5, 18);
d3 -= -100;
d3.Print();
d3 += 10000;
d3.Print();
d3 -= 10000;
d3.Print();
}
运行结果:
🎁5.5 - (Date-整数)
Date Date::operator-(int day)
{
Date ret = *this;
ret -= day;
return ret;
}
🎅5.6 前置++和后置++
前置++和后置++运算符重载都是Date operator++();
如何区分呢?
C++在语法上给了一些特殊处理:要想区分,必须构成函数重载,函数名相同,参数必须不同。
Date operator++();
//只是为了区分,参数不需要具体的值
Date operator++(int);
C++语法规定不带参数的是前置,带参数的是后置。
实现起来很简单,我们直接调+=
// 前置++
// ++d -> d.operator++(&d)
Date& operator++()
{
*this += 1;
return *this;
}
后置++要返回++之前的值,可以先拷贝一个,但是ret出了作用域就销毁了,不能用引用返回。
// 后置++
// d++ -> d.operator++(&d, 0)
Date operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
测试及运行结果:
🎄5.7 日期-日期 返回天数
首先,日期-日期和日期-天数是构造函数重载的。日期-天数的参数是int,日期-日期的参数是日期类。
博主教大家用一种最简单的方法来实现。
我们不知道d1、d2谁小谁大,我们可以先默认d1大,d2小,后面再修正。flag来代替±号。
// d1 - d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
//修正
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
int n = 0;
//复用!=和前置++
//max和min相差几天n就+几天
while (min != max)
{
++n;
++min;
}
return n * flag;
}
测试及运行结果:
🔮6 关于拷贝构造函数的一些思考
📷6.1 Date d2 = d1
void TestDate2()
{
Date d1(2022, 5, 18);
Date d2 = d1;
}
大家觉得Date d2= d1;
是拷贝构造还是赋值?
其实它也会去调拷贝构造函数,而不是赋值运算符重载。
结论:
int main()
{
Date d1(2022, 5, 16);
Date d2(2022, 5, 18);
Date d3(d1); // 拷贝构造 -- 一个存在的对象去初始化另一个要创建的对象
Date d3 = d1;// 拷贝构造 -- 一个存在的对象去初始化另一个要创建的对象
d2 = d1; // 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
return 0;
}
💻6.2 传值返回的值做引用参数不加const报错
d1+15这个表达式的结果是:传值返回其实返回的是拷贝了ret的临时变量,临时变量具有常性(只能读不能写),再去调用拷贝构造或赋值运算符重载时,权限被放大了,当然会报错。
📗7 完整代码
📘7.1 Date.h
#include <iostream>
#include <assert.h>
using std::cin;
using std::cout;
using std::endl;
class Date
{
// 友元函数
friend std::ostream& operator<<(std::ostream& out, const Date& d);
friend std::istream& operator>>(std::istream& out, Date& d);
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
if (year >= 1 &&
month <= 12 && month >= 1 &&
day >= 1 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法访问" << endl;
}
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
this->_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
bool operator<(const Date& d);
bool operator==(const Date& d);
// d1 <= d2
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);
}
Date& operator-=(int day);
Date& operator+=(int day);
Date operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
Date operator-(int day)
{
Date ret = *this;
ret -= day;
return ret;
}
int operator-(const Date& d);
// 前置++
// ++d -> d.operator++(&d)
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
// d++ -> d.operator++(&d, 0)
Date operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
// // 后置--
Date operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
📓7.2 Date.cpp
#include "Date.h"
std::ostream& operator<<(std::ostream& out, const Date& d)
{
out <<d. _year << "-" << d._month << "-" << d._day << endl;
return out;
}
std::ostream& operator>>(std::ostream& int, Date& d)
{
in >>d. _year >> "-" >> d._month >> "-" >> d._day >> endl;
return in;
}
bool Date::operator<( const Date& d)
{
if ((_year < d._year)
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && d._day < d._day))
{
return true;
}
else
{
return false;
}
}
// d1 == d2
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool isLeapYear(int year)
{
if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))
{
return true;
}
return false;
}
int Date::GetMonthDay(int year, int month)
{
assert(year >= 0 && month > 0 && month < 13);
const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && isLeapYear(year))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
// d1 -= 100
Date& 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;
}
Date& Date::operator+=(int 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 - d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
min = *this;
max = d;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
大家如果需要菜单,自己去写吧。希望大家下来自己去实现一遍,只有消化了,知识才属于自己。
❗️8. 拓展:>> 和 <<
流提取(>>)和流插入(<<)运算符,这两个运算符想要重载就得引入一个东西。
我们知道内置类型是直接支持这两个运算符的,并且能自动识别类型,大家知道编译器是如何自动识别的吗?cin和cout又是什么东西呢?
cin和cout其实是全局的对象,它们包含在 <iostream>
这个头文件里面,所以我们平时要包这个<iostream>
头文件。cin是istream
的对象,cout是ostream
的对象。所以说istream
和ostream
是两个类型,这两个类型是库里面的。
我们内置类型可以直接用,是因为C++已经帮你把内置类型重载了。自动识别类型本质上源自于函数重载。
我们是自定义类型,那我们也写一个。假设流插入,那这里就是ostream的对象,因为我们需要cout,out就是cout的别名。
void operator<<(std::ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
}
如果这样写,那就要d1 << cout;
(d1.operator<<(cout);
)这样调用,可是这样调用也太戳了,把cout流插入到d1里面?
因为cout << d1;
这样调用等价于cout.operator<<(d1);
这样调用。这里的<<
流插入是作为cout的成员函数,只支持内置类型,不支持自定义类型。我们不可能把ostream类型的代码给改了,cout是库里面写的,我们不能更改。
我们知道,对于有两个操作数的运算符,左边的操作数是必然要传给this指针,我们动不了。那怎么办?有没有方式可以解决一下呢?
我们只要把ostream类型的cout对象作为第一个参数就成了,成员函数就没有机会了,那我们把它写成全局的。
void operator<<(std::ostream& out,const Date& d)
{
out <<d. _year << "-" << d._month << "-" << d._day << endl;
}
但是这样又有一个问题:成员变量是私有的,我们访问不了。
我们可以用友元函数来解决,友元可以加在任意位置。
class Date
{
// 友元函数
friend std::ostream& operator<<(std::ostream& out, const Date& d);
friend std::istream& operator>>(std::istream& out, Date& d);
public:
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int GetMonthDay(int year, int month);
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
这样>>和<<的运算符重载函数就是这个类的朋友了,就可以访问私有的成员变量了。
cout << d1 << d2;
operator<<(cout, d1);
但是这个<<流插入的全局函数不能定义在Date.h里面,因为Date.cpp和Test.cpp(假设是你们写的菜单文件)同时包含这个头文件就会链接冲突(因为两个cpp文件编译后就会产生两个.o文件,两个.o文件里的符号表都有这个函数)。所以全局函数一般不写在.h文件里。
类里面定义的函数之所以链接不冲突,是因为默认是内联函数,不会放进符号表的。
所以全局函数在.h文件里声明,在Date.cpp里定义。
还有一个坑哦:如果要支持连续流插入,函数就必须有一个返回值。 所以我们必须这样玩:
std::ostream& operator<<(std::ostream& out, const Date& d)
{
out <<d. _year << "-" << d._month << "-" << d._day << endl;
return out;
}
cin(>>)也同理:这个时候日期类就不用const修饰了,因为你提取到的值要放进d里。
std::ostream& operator>>(std::ostream& int, Date& d)
{
in >>d. _year >> "-" >> d._month >> "-" >> d._day >> endl;
return in;
}
流提取为了防止提取非法日期,还得加一个检查,这里我就不写了。