类和对象拓展——日期类
一.前言
通过前面对类和对象的学习,现在我们可以开始实践日期类的代码编写。在实际操作过程中,我会补充之前文章中未提及的相关知识点。
二.正文
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成员函数} };
总结:
- const 对象调用非 const 成员函数:禁止。非 const 成员函数可能修改对象状态,与 const 对象的只读特性相冲突。
- 非 const 对象调用 const 成员函数:允许。const 成员函数保证不修改对象状态,与非 const 对象兼容,且可复用只读逻辑。
- const 成员函数调用非 const 成员函数:禁止。非 const 成员函数可能修改对象状态,违反 const 成员函数的只读约束。
- 非 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了?
因为两个都要修改。流提取要改变里面的状态值,使用不用。
调用结果:
三.总结
希望这个日期类的知识能对你有所帮助!如果觉得实用,欢迎点赞支持~ 要是发现任何问题或有改进建议,也请随时告诉我。感谢阅读!