当前位置: 首页 > news >正文

类和对象拓展——日期类

一.前言

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

二.正文 

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了?

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

调用结果:

三.总结

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

http://www.dtcms.com/a/270705.html

相关文章:

  • 【实习篇】之Http头部字段之Disposition介绍
  • 使用 Docker 搭建 Rust Web 应用开发环境——AI教你学Docker
  • VR重现红军过雪山:一场穿越时空的精神洗礼​
  • MySQL 09 普通索引和唯一索引
  • MySQL 间隙锁
  • pytorch 自动微分
  • 半导体晶圆检测的基本知识
  • EGARCH
  • Linux C 目录流基本操作
  • Alloy VS Promtail:基于 Loki 的日志采集架构对比与选型指南
  • ECS由浅入深第四节:ECS 与 Unity 传统开发模式的结合?混合架构的艺术
  • Using Spring for Apache Pulsar:Publishing and Consuming Partitioned Topics
  • vue2 echarts中国地图、在地图上标注经纬度及标注点
  • AI应用实践:制作一个支持超长计算公式的计算器,计算内容只包含加减乘除算法,保存在一个HTML文件中
  • 「macOS 系统字体收集器 (C++17 实现)」
  • Oracle存储过程导出数据到Excel:全面实现方案详解
  • Java零基础笔记08(Java编程核心:面向对象编程高级 {继承、多态})
  • 【macOS】【Swift】【RTF】黑色文字在macOS深色外观下看不清的解决方法
  • yolo8实现目标检测
  • springMVC05-异常处理器
  • HashMap源码分析:put与get方法详解
  • 【拓扑空间】示例及详解1
  • sqlplus表结构查询
  • 高效集成-C#全能打印报表设计器诞生记
  • Android-重学kotlin(协程源码第一阶段)新学习总结
  • mongodb: cannot import name ‘_check_name‘ from ‘pymongo.database‘
  • 池化思想-Mysql异步连接池
  • 教育行业可以采用Html5全链路对视频进行加密?有什么优势?
  • 高通 QCS6490PI 集群架构支撑 DeepSeek 模型稳定运行的技术实现
  • upload-labs靶场通关详解:第19关 条件竞争(二)