C++类和对象实战:实现一个日期类

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 类的默认成员函数

思考我们需要自己实现哪些成员函数?

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符重载
  5. 取地址重载
  6. 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. 完整类关系图

Date
-int _year
-int _month
-int _day
+各种运算符重载
+Date(int, int, int)
+GetMonthDay() : int
+operator<<()
+operator>>()

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++核心概念:

  1. 类的封装设计
  2. const引用的适用场景
  3. 运算符重载原则
  4. 友元函数的使用场景
  5. 代码复用技巧
  6. 异常处理机制

最佳实践

  • 优先实现复合赋值运算符(如+=)
  • 使用const修饰不修改对象的成员函数
  • 对输入参数进行有效性检查
  • 合理使用友元函数维护封装性

完整实现代码可在GitHub仓库获取:github地址
以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流

分享到此结束啦
一键三连,好运连连!

猜你喜欢

转载自blog.csdn.net/2301_80064645/article/details/145618202
今日推荐