1. 前言
在面向对象编程中,类的设计和实现是核心内容。本文通过实现一个日期类,综合运用构造函数、运算符重载、友元函数等C++特性
,帮助读者巩固面向对象编程知识。我们将实现日期的基本操作,包括日期比较、日期加减、流输入输出等功能。
2. 类体设计
#include <assert.h>
#include <iostream>
using namespace std;
//实现一个日期类,表示日期的话,全是内置类型int成员
class Date {
public:
Date(int year = 2025, int month = 2, int day = 22) {
//要从构造处和输入处检查非法日期
//由于要检查非法日期,因此不便使用初始化列表初始化
if (month > 0 && month < 13 && day > 0
&& day < GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else {
cout<<"非法日期"<<endl;
assert(false);
}
}
void Print() const {
cout << _year << "--" << _month << "--" << _day << endl;
}
//获取某年中某个月的天数
int GetMonthDay(int year, int month) const {
//局部静态
const static int daysArray[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 daysArray[month];
}
//如果要用引用返回,29是常量,需要着重考虑
//内置类型没必要用引用返回,引用的底层也需要开指针,差距不大
}
//比较类运算符
bool operator<(const Date& d) const ;
bool operator==(const Date& d) const ;
bool operator>(const Date& d) const ;
bool operator>=(const Date& d) const ;
bool operator<=(const Date& d) const ;
bool operator!=(const Date& d) const;
//计算类运算符
Date operator+(const int day) const ; //+不会改变对象本身
Date& operator+=(const int day); //有返回值,是为了满足+=的连续赋值
Date& operator++(); //前置++ 编译器规则,前置++重载时,直接重载
Date operator++(int); //后置++ 后置++重载时,要加一个参数用于占位,来构成函数++的重载
Date operator-(const int day) const;
Date& operator-=(const int day);
Date& operator--();
Date operator--(int);
int operator-(const Date& d) const; //实现两个日期的相减
//流插入不能写成成员函数?
//因为Date对象默认占用第一个参数,就是做了左操作数
//写出来就一定是下面这样子,不符合使用习惯
// d1 << cout; // d1.operator<<(cout); //第一个参数是左操作数,第二个参数是右操作数
// cout << d1; 要让cout成为第一个参数,把<<重载成全局的
//重载成全局的之后,利用友元来实现 全局函数对类内成员的访问
//友元函数声明 可以使类外的函数 不受访问权限的控制
friend ostream& operator<<(std::ostream& out, const Date& d);
friend istream& operator>>(std::istream& in, Date& d);
private:
int _year;
int _month;
int _day;
};
3. 类成员函数的实现
3.1 类的默认成员函数
思考我们需要自己实现哪些成员函数?
- 构造函数:
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- 取地址重载
- const取地址重载
日期类,成员变量用来表示年月日,只需内置数据类型就可以完成表示,不需要定义自定义类型。
- 构造函数:需要自己实现,可能需要设置构造函数的缺省参数为当天日期。
- 析构函数,拷贝构造函数,赋值运算符重载:类中成员变量的类型均为内置类型,且没有开在堆上申请资源。
- 不需要析构函数清理资源。
- 编译器生成的拷贝构造函数(浅拷贝)可以完成对内置类型的拷贝。
- 编译器生成的赋值运算符重载可以完成
Date
类对象的赋值(内置类型浅拷贝即可)
- 取地址重载和const取地址重载:我们无需改变使用者对对象进行取地址时获得的结果,无需实现。
3.2 比较运算符家族
- 我们的比较运算符,仅对日期进行比较,不涉及对成员变量进行修改,因此应当全部加上
const
- 参数使用
const Date& d
,是日期类对象的const
引用,直接使用原对象的别名,减少对象拷贝时的性能开销,并且使用const
确保原对象不被修改。
3.2.1 <运算符重载
bool Date::operator<(const Date& d) const {
if (this->_year < d._year)
return true;
else if (this->_year == d._year && this->_month < d._month)
return true;
else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)
return true;
else
return false;
}
- 先考虑小于的情况:
- 年份小就是小。
- 年相同时,月份小就是小。
- 年份月份都相同时,天小就是小。
- 小于返回
true
- 不小于的情况就返回
false
3.2.2 ==运算符重载
bool Date::operator==(const Date& d) const {
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
- 两个日期相等,
年月日都相等才是两个日期的相等
。
3.2.3 其他运算符重载
我们已经实现了小于和等于的重载,其他比较关系可以直接结合‘!’来实现
利用逻辑关系复用以上代码。
3.2.3.1 <=重载
bool Date::operator<=(const Date& d) const {
return *this < d || *this == d;
}
<=
就是小于或等于。
3.2.3.2 >重载
bool Date::operator>(const Date& d) const {
return !(*this <= d);
}
>
就是<=
的非
3.2.3.3 >= 重载
bool Date::operator>=(const Date& d) const {
return !(*this < d);
}
>=
就是<
的非
3.2.3.4 != 重载
bool Date::operator!=(const Date& d) const {
return !(*this == d);
}
!=
就是==
的非
3.3 算数运算符
+=
和-=
是对原对象进行修改,因此用引用返回,且不能声明为const
成员函数+
和-
是要返回+或-后的日期,对原对象没有修改,因此,只能返回局部对象的拷贝
3.3.1 日期加天数
+=
//这样写 是+= 会修改当前对象
Date& Date::operator+=(const int day) {
if (day < 0) {
return *this -= -day;
}
this->_day += day; //修改了当前对象,所以实现的是+=
while(this->_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13) {
++_year;
_month = 1;
}
}
return *this;
}
- 定义
+=
运算符,参数为int
型天数,返回当前对象的引用。 - 若天数为负,转换为调用
-=
运算符,避免重复代码。 - 直接增加天数到当前日期的
_day
成员。 - 循环处理天数溢出。当
_day
超过当月天数时,减去该月天数,月份递增。若月份超过12,年份递增,月份重置为1。 - 返回当前对象,支持链式操作(如
d1 += 5 += 3;
)。
+
Date Date::operator+(const int day) const {
//拷贝构造一个
Date temp(*this); //Date temp = *this; //两种写法本质上是一样的
temp += day;
return temp;
}
- 定义
+
运算符,不修改原对象,返回新对象。 - 通过拷贝构造函数创建临时对象
temp
,复制当前对象状态。 - 调用
temp
的+=
运算符,实现天数累加。 - 返回临时对象的拷贝。由于是值返回,调用者得到的是新对象的副本。
前置++
- 前置
++
,返回++
之后的值
Date& Date::operator++() {
*this += 1;
return *this;
}
- 定义前置
++
运算符,无参数。 - 调用
+=
运算符增加1天。 - 返回当前对象的
引用
,符合前置++
的语义(返回递增后的值)。
后置++
- 后置++,返回++之前的值
- 运算符重载本质是函数,因此可以用函数重载来实现前置++和后置++的同时存在
Date Date::operator++(int) {
Date temp(*this);
*this += 1;
return temp;
}
- 增加这个int参数不是为了接收具体的值,仅仅是为了占位,与前置++构成重载
- 定义后置
++
运算符,使用int
占位参数以区分前置版本。 - 创建临时对象
temp
,拷贝原对象。 +=
递增当前对象。- 返回临时对象(原对象++前的值),符合后置
++
的语义(返回递增前的值)。
3.3.2 日期减天数
-=
Date& Date::operator-=(const int day) {
if (day < 0) {
//考虑输入为负数的情况, - 复用了 -= ,因此只需要修改-=函数
return *this += -day;
}
this->_day -= day;
while (_day <= 0) {
--_month; //--之后,可能-完之后变成0
while (_month <= 0) {
_year--;
_month = 12;
}
//借完位才可以加天数
_day += GetMonthDay(_year, _month);
}
return *this;
}
若天数为负
,转换为调用+=
运算符。- 直接减少天数,可能导致
_day
为负。 - 循环处理
_day
为负的情况。递减月份,若月份为0,年份递减,月份设为12。然后加上前一个月的天数,直到_day
为正。
-
Date Date::operator-(const int day) const {
Date temp = *this; //此处是 拷贝构造
temp -= day;
return temp;
}
- 拷贝当前对象到临时对象
temp
。 - 调用
-=
运算符减少天。 - 返回修改后的临时对象的拷贝。
前置–
//前置--
Date& Date::operator--() {
*this -= 1;
return *this;
}
- 调用
-=
运算符减少1天。 - 返回对象
--
后的引用。
后置–
Date Date::operator--(int) {
//占位参数,构成参数
Date temp(*this);
*this -= 1;
return temp;
}
- 保存当前对象状态到
temp
。 - 当前对象
-=
1。 - 返回原对象的拷贝。
3.3.3 前置和后置对比
类型 | 返回值 | 性能 |
---|---|---|
前置++/– | 引用 | 更优 |
后置++/– | 临时对象 | 较低效 |
性能方面,内置类型前置和后置的效率差别不大,自定义类型后置++/–会多出原对象的拷贝操作。
因此,如果是自定义类型,要多使用前置++
- 如果要追求极致的性能,则可统一使用前置++
3.3.4 两个日期的相减
int Date::operator-(const Date& d) const {
Date max = *this; // 假设当前对象较大
Date min = d;
int flag = 1; // 结果符号标记
if (*this < d) {
// 判断实际大小
max = d;
min = *this;
flag = -1;
}
int days = 0;
while (min < max) {
// 逐天递增min直到等于max
++min;
++days;
}
return days * flag; // 返回带符号的天数差
}
- 确定较大和较小的日期,并设置结果符号。
- 通过循环递增较小的日期,直到与较大日期相等,统计天数差。
- 返回带符号的天数差,确保结果的正确性。
3.4 输入输出重载
关键点:
- 使用友元函数访问私有成员
- 返回流引用支持链式调用
3.4.1 <<流插入重载
//重载成全局函数,没办法访问私有成员变量,C++中会用友元来解决
//ostream operator<<(const ostream& out, const Date& d) //流插入就是往cout里插入数据,如果加了const,还怎么插入
ostream& operator<<(std::ostream& out, const Date& d) {
out << d._year << "年--" << d._month << "月--" << d._day << "日";
return out; // 支持链式输出
}
- 定义全局
<<
运算符,参数为输出流和日期对象。 - 将日期的年、月、日格式化输出到流。
- 返回
ostream
的引用,支持链式调用(如cout << d1 << d2;
)。
3.4.1 >>流提取重载
//流提取 cin 不能加const, 因为提取后会,内部会更改状态值
istream& operator>>(std::istream& in, Date& d) {
int year, month, day;
in >> year >> month >> day; // 读取输入
// 检查日期合法性
if (month >= 1 && month <= 12 && day > 0 && day <= d.GetMonthDay(year, month)) {
d._year = year;
d._month = month;
d._day = day;
} else {
cerr << "非法日期" << endl;
assert(false); // 终止程序
}
return in; // 返回流引用
}
- 从输入流读取年、月、日。
- 检查月份和天数的合法性。
GetMonthDay
获取当月最大天数。 - 若合法,设置日期对象的成员。
- 非法日期输出错误信息,断言终止程序。
- 返回流引用,支持链式输入。(如
cin>>d1>>d2;
)。
4. 完整类关系图
5. 关键问题解析
1. 为什么使用静态数组存储月份天数?
- 避免每次调用函数时重复创建数组
- 使用static关键字使数组仅初始化一次
2. 如何处理闰年二月天数?
- 闰年判断条件:
(year%4==0 && year%100!=0) || (year%400==0)
3. 为什么选择先实现+=再实现+?
- 提高代码复用性
- 减少重复代码和临时对象的拷贝,提高性能
- 符合运算符重载的最佳实践
3.1 +复用+=
+
复用+=
仅需创建两个临时对象- 第一个:是拷贝构造的临时对象
- 第二个:是值返回时,对返回临时变量的拷贝。
3.2 +=复用+
+=
复用+
则需创建4
个临时对象- 第一个:是
+
中拷贝构造的临时对象 - 第二个:是
+中
值返回时,对返回临时变量的拷贝。 - 第三个和第四个:是
+=
中调用了重载的+
,+
中还需要再创建两个临时对象。
在
-
中复用已实现的-=
也是同理。
6. 使用示例
int main() {
Date d1(2025, 2, 28);
Date d2 = d1 + 3; // 2025-3-3
cout << "d1: " << d1 << " d2: " << d2 <<endl;
cout << "相差天数: " << d2 - d1 << endl;
Date d3;
cin >> d3; // 输入验证示例
cout << d3;
return 0;
}
7. 总结
通过实现日期类,我们实践了以下C++核心概念:
- 类的
封装
设计 const
和引用
的适用场景运算符重载
原则友元
函数的使用场景- 代码
复用
技巧 异常处理
机制
最佳实践:
- 优先实现复合赋值运算符(如+=)
- 使用const修饰不修改对象的成员函数
- 对输入参数进行有效性检查
- 合理使用友元函数维护封装性
完整实现代码可在GitHub仓库获取:github地址
以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流
分享到此结束啦
一键三连,好运连连!