类和对象拓展——日期类

发布于:2025-07-10 ⋅ 阅读:(24) ⋅ 点赞:(0)

一.前言

通过前面对类和对象的学习,现在我们可以开始实践日期类的代码编写。在实际操作过程中,我会补充之前文章中未提及的相关知识点。

二.正文 

1. 日期类代码实现

我们先来看看要实现什么功能吧,把他放在Date.h中

#pragma once
#include<iostream>
using namespace std;

class Date
{
public:
	// 全缺省的构造函数
	Date(int year = 2025, int month = 7, int day = 7);

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	// 拷贝构造函数
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 运算符重载
	bool operator<(const Date& x);
	bool operator==(const Date& x);
	bool operator<=(const Date& x);
	bool operator>(const Date& x);
	bool operator>=(const Date& x);
	bool operator!=(const Date& x);

	// 获取某年某月的天数
	int GetMonthDay(int year, int month);

	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);

	// 日期-=天数
	Date& operator-=(int day);
	// 日期-天数
	Date operator-(int day);

	// 日期-日期  返回天数
	int operator-(const Date& x);

	// 前置++
	Date& operator++();
	// 后置++
	Date operator++(int);

	// 前置--
	Date& operator--();
	// 后置--
	Date operator--(int);

private:
	int _year;
	int _month;
	int _day;
};

继续像之前一下在准备两个文件,Date.cpp和test.cpp

 1.构造函数

类的对象在创建时会自动调用构造函数,若未显式定义,编译器会生成默认构造函数( Date() ),但默认构造函数不会初始化成员变量。若成员变量未初始化,会出现年月日随机值,会导致对象状态无意义,后续操作必然出错。

Date::Date(int year, int month, int day)
{
	if (month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
	}
}

为什么博主选择全缺省参数的构造函数了?

1.构造对象更灵活:支持多种初始化方式 

Date d1;
Date d2(2025);
Date d3(2025, 7);
Date d4(2025, 7, 8);

都可以初始化

2.简化接口设计:减少构造函数重载

非全缺省构造函数需要重载

Date();//无参构造(默认日期)
Date(int year);//仅传年份
Date(int year, int month);//传年份和月份
Date(int year, int month, int day); //全参数构造

而全缺省构造函数只需要一个

3.增强代码可维护性:默认值统一管理

这时候有人好奇了,那析构函数要吗?

不需要的,原因如下:

成员变量无动态资源

存储在栈上或类对象的内存空间中,生命周期结束时会被系统自动释放,无需手动处理。但是当遇到开辟了动态空间,则需要大家自己写析构函数了。 


 2.打印 

为了方便后续的检查代码正确性,写一个打印更方便看到结果。又因为他很简短可以直接放在类里面。

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

3.拷贝构造函数 

可以直接写在类里面

	// 拷贝构造函数
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

或者类里面声明,类外面实现如下:

class Date
{
public:
	//省略其他的功能
	Date(const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

来看看运行结果吧:


4.关系运算符重载 

要写一系列 >  >=  < <= ==  !=关系运算符,我们可以先写一组 < 和 == ,或者 > 和 ==,然后就可以用复用。

// <运算符重载
bool Date::operator<(const Date& x)
{
	if (_year < x._year)
		return true;
	else if (_year == x._year && _month < x._month)
		return true;
	else if (_year == x._year && _month == x._month && _day < x._day)
		return true;

	return false;
}
//==运算符重载
bool Date::operator==(const Date& x)
{
	return _year == x._year
		&& _month == x._month
		&& _day == x._day;
}

然后就可以复用了

// <=运算符重载:直接调用上面写的那两个,满足其中一个就好了,所以 ||
bool Date::operator<=(const Date& x)
{
	return *this < x || *this == x;
}
// < 运算符重载:可直接调用上一个复用
bool Date::operator>(const Date& x)
{
	return !(*this <= x);
}
// >=运算符重载
bool Date::operator>=(const Date& x)
{
	return !(*this < x);
}
// !=运算符重载
bool Date::operator!=(const Date& x)
{
	return !(*this == x);
}

看看运行结果: 


 5.算数运算符

日期加天数

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;
}
Date Date::operator+(int day)
{
	Date tmp(*this);

	tmp += day;

	return tmp;
}

因为可以连续赋值,所以要有返回值,又因为+=改变了本身,所以用Date&,返回*this就好了。 而+不改变本身,所以要先定义一个tmp然后返回tmp。

 +的代码不调用一个为

Date Date::operator+(int day)
{
	Date tmp(*this);
	if (day < 0)
	{
		return tmp - (-day);
	}
	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;
}

为什么是+调用+=而不是+=调用加啦?

1.使用 operator+=  调用 operator+  形成 operator+=  函数

Date& Date::operator+=(int day)
{
    *this = *this + day;
    return *this;
}

在 Date& Date::operator+=(int day)  函数中,调用 operator+  时会创建一个临时对象(因为 operator+  通常会返回一个新对象),然后将这个临时对象赋值给 *this 。*this + day  会创建一个临时对象,之后再将其内容复制给当前对象,这就产生了额外的对象创建和销毁开销。建立了两个临时变量

2.使用 operator+  调用 operator+=  形成 operator+  函数

Date Date::operator+(int day)
{
    Date tmp(*this);
    tmp += day;
    return tmp;
}

在 Date Date::operator+(int day)  函数中,仅创建了一个临时对象 tmp ,然后调用 operator+=  直接在这个临时对象上进行日期累加操作。这里只创建了一个 tmp  对象用于存储计算结果。后续不需要再创建额外的临时对象来完成加法操作。

总结:

"使用 operator+ 调用 operator+= 形成 operator+ "函数的方式更好,它能减少临时对象创建、避免多次内存分配与释放、利用编译器优化,提高代码执行效率。


 日期减天数

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		_day += GetMonthDay(_year, _month);
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}
	return *this;
}
Date Date::operator-(int day)
{
	Date tmp(*this);
	tmp -= day;

	return tmp;
}

因为可以连续赋值,所以要有返回值,又因为-=改变了本身,所以用Date&,返回*this就好了。 而-不改变本身,所以要先定义一个tmp然后返回tmp。 

  -的代码不调用一个为

Date Date::operator-(int day)
{
	Date tmp(*this);
	if (day < 0)
	{
		return tmp + (-day);
	}
	tmp._day -= day;
	while (tmp._day <= 0)
	{
		--tmp._month;
		if (tmp._month == 0)
		{
			--tmp._year;
			tmp._month = 12;
		}
		tmp._day += GetMonthDay(tmp._year, tmp._month);
	}

	return tmp;
}

1.使用 operator-=  调用 operator-  形成 operator-=  函数

Date& Date::operator-=(int day)
{
	*this = *this - day;

	return *this;
}

2.使用 operator-  调用 operator-=  形成 operator-  函数

Date Date::operator-(int day)
{
	Date tmp(*this);
	tmp -= day;

	return tmp;
}

为什么博主使用"使用 operator- 调用 operator-= 形成 operator- "呢?理由也就是上面一样了。

日期减日期 

int Date::operator-(const Date& x)
{
	Date min = *this;
	Date max = x;
	int flag = 1;

	if (*this > x)
	{
		min = x;
		max = *this;
		flag = -1;
	}
	int dayCount = 0;
	while (min < max)
	{
		min++;
		dayCount++;
	}

	return dayCount * flag;
}

这时候有人可能好奇为什么要定义一个flag?

因为如果当前对象(*this)小于x则flag为正,然后乘dayCount则为正,即表示x在当前对象之后,相差dayCount天

而若当前对象比x大则flag为负,然后乘dayCount则为负,即表示当前对象在x之后,相差dayCount天

看看运行结果:  


6.自增自减运算符 

前置++

Date& Date::operator++()
{
	*this += 1;
	return *this;
}

 后置++

Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

前置--

Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

后置-- 

Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

后置++和后置--括号中的int无实际意义只是为了构成重载,易于区分

前置版本( operator++() 、 operator--() ):先加加后使用,先修改当前对象,再返回自身引用( *this )
后置版本( operator++(int) 、 operator--(int) ):先使用后加加,先复制当前对象( tmp ),再修改自身,最后返回修改前的副本。 

运行结果:

上述就是日期类的大致代码,但是我们还能进行优化,让我们继续学习吧。

2.日期类代码的优化

1.const成员 

首先来学习一下const成员

const成员函数:用const修饰的“成员函数”,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

因为Print不改变任何成员使用可以加,大家伙可以想想我们上面的日期类代码还有哪里可以加 

若声明和定义分开 ,两个都得写,如下:

我给大家吧要加的写出来

void Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

bool operator<(const Date& x) const;
bool operator==(const Date& x) const;
bool operator<=(const Date& x) const;
bool operator>(const Date& x) const;
bool operator>=(const Date& x) const;
bool operator!=(const Date& x) const;

Date operator+(int day) const;
Date operator-(int day) const;
int operator-(const Date& x) const;

 因为他们不改变任何成员使用可以加。

可能有人好奇为什么要加?又得判断还可能加错。

因为有些使用的人初始化会写成 const Date d2(2025, 11, 22);

使用const是有一定好处的。

只要成员内部不修改成员变量,都应该加const,这样const对象和普通对象都可以调用。权限可以缩小和转移,不能放大。

来看看几题思考题

1. const对象可以调用非const成员函数吗?

不可以。

const对象表示其状态不能被改变。非const成员函数有可能会修改对象的数据成员(因为其没有承诺不修改对象状态),若允许const对象调用非const成员函数,就可能违背const对象不可变的特性。

class MyClass {
public:
    void nonConstFunc() {
        data = 10; // 可能修改对象数据成员
    }
private:
    int data;
};
const MyClass obj;
obj.nonConstFunc(); // 编译错误,const对象不能调用非const成员函数

2. 非const对象可以调用const成员函数吗?

可以。

const成员函数承诺不会修改对象的数据成员,对于非const对象而言,调用const成员函数不会有违背其可变性的问题,而且这也提供了一种在不同场景下灵活调用函数的方式。

class MyClass {
public:
    void constFunc() const {
        // 这里不会修改对象数据成员
    }
};
MyClass obj;
obj.constFunc(); // 合法,非const对象可以调用const成员函数

3. const成员函数内可以调用其它的非const成员函数吗?

不可以。

const成员函数保证不会修改对象状态,而调用非const成员函数可能会改变对象的数据成员,这就破坏了const成员函数的承诺。

class MyClass {
public:
    void nonConstFunc() {
        data = 10;
    }
    void constFunc() const {
        nonConstFunc(); // 编译错误,const成员函数不能调用非const成员函数
    }
private:
    int data;
};

4. 非const成员函数内可以调用其它的const成员函数吗? 

可以。

非const成员函数本身就可以修改对象状态,但调用const成员函数不会有问题,因为const成员函数不会改变对象状态,不会破坏非const成员函数对对象状态修改的灵活性 。

class MyClass {
public:
    void constFunc() const {
        // 不修改对象数据成员
    }
    void nonConstFunc() {
        constFunc(); // 合法,非const成员函数可以调用const成员函数
    }
};

总结:

  1. const 对象调用非 const 成员函数:禁止。非 const 成员函数可能修改对象状态,与 const 对象的只读特性相冲突。
  2. 非 const 对象调用 const 成员函数:允许。const 成员函数保证不修改对象状态,与非 const 对象兼容,且可复用只读逻辑。
  3. const 成员函数调用非 const 成员函数:禁止。非 const 成员函数可能修改对象状态,违反 const 成员函数的只读约束。
  4. 非 const 成员函数调用 const 成员函数:允许。非 const 函数可以安全调用只读的 const 函数,既不影响自身逻辑,又能实现代码复用。

2.取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。这两个也比较少用。这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容或者不想让人取到普通对象的地址。

1.想让别人获取到指定的内容

class Point {
private:
	int x, y;
public:
    //下面是初始化列表可以先不要太在意,下一篇会介绍
	Point(int a, int b)
		:x(a)
		, y(b)
	{}
	// 取地址时返回x的地址(而非Point对象地址)
	int* operator&()
	{
		return &x;
	}
};

// 使用:
Point p(10, 20);
int* px = &p; // px指向p.x,而非p本身

2.不想让人取到普通对象的地址 

class NoNormalAddr {
public:
	// 普通对象取地址返回空
	NoNormalAddr* operator&()
	{
		return nullptr;
	}
	const NoNormalAddr* operator&() const
	{
		return this;
	}
};

// 使用:
NoNormalAddr obj;
const NoNormalAddr c_obj;
&obj; // 得到nullptr
&c_obj; // 得到实际地址

可能有人那代码去VS中尝试了,发现会红为什么了?

简单说:取地址操作本身不报错,但返回的 nullptr  是无效地址,用它做后续操作时才会因“访问无效内存”而报错。这是一种通过返回无效结果间接阻止滥用地址的方式。


3.流插入和流提取 

先来看看这个图片简单了解一下

流插入<< 

先看看我们用operator的常规思想 

void Date::operator<<(ostream& out)
{
	out << _year << "-" << _month << "-" << _day << endl;
}

void test7()
{
	Date d1(2025, 1, 25);
	d1 << cout;//d1.operator<<(cout);
}

发现调用的时候不符合我们常规调用,因为成员函数第一个参数是隐藏的this,使用调用就成这样了。为了正常,我们要将他写在全局,这样this就不占用参数了。且我们还可能连续插入,修改后的:

糟糕爆红了,发现_year,_month,_day 位于私有调用不到怎么办了?

方法一:友元函数,在类里面加入

friend ostream& operator<<(ostream& out, const Date& d);

此时就可以调用了,调用结果:

此时就符合习惯了。

友元

我们来学一下友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

友元分为:友元函数和友元类

 友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

//frinnd+函数名
friend ostream& operator<<(ostream& out, const Date& d);

注意:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

class Time
{
	friend class Date;
	// 声明日期类为时间类的友元类,
	// 则在日期类中就直接访问Time类中的私有成员变量
public:
	...;//博主懒了
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	...;//博主懒了
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

注意:

  • 友元关系是单向的,不具有交换性。 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递。如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。

 方法二:定义一个函数用来获取_year,_month,_day

//写在类里面
int GetYear() const
{
	return _year;
}

int GetMonth() const
{
	return _month;
}

int GetDay() const
{
	return _day;
}

流提取>>

 按照上面的学习:

//类里面
friend istream& operator>>(istream& cin, Date& d);

istream& operator>>(istream& in, Date& d)
{
	int year, month, day;
	in >> year >> month >> day;

	if (month > 0 && month < 13
		&& day > 0 && day <= d.GetMonthDay(year, month))
	{
		d._year = year;
		d._month = month;
		d._day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		assert(false);
	}

	return in;
}

 为什么没const了?

因为两个都要修改。流提取要改变里面的状态值,使用不用。

调用结果:

三.总结

希望这个日期类的知识能对你有所帮助!如果觉得实用,欢迎点赞支持~ 要是发现任何问题或有改进建议,也请随时告诉我。感谢阅读!


网站公告

今日签到

点亮在社区的每一天
去签到