文章约六万余字,篇幅较长,建议电脑端访问
学习了类和对象的封装后,了解了一个类的基本组成以及如何去实例化一个类,今天就让我们进到类的内部,来探一探类里面都有哪些东西
Stack st;
st.Push(1);
st.Push(2);
st.Push(3);
Init()
初始化了,加上之后就没有问题了Destroy()
,时间久了便会造成【内存泄漏】Destroy()
之后才是一个完整的从定义一个栈、到使用、销毁一个栈全过程return false
,但此时呢在return之前又要把定义出来的栈给销毁了才行,在力扣上是没关系,也不会给你保存内存泄漏的警告,不过为了【严谨性】,便需要考虑到这一点bool isValid(char * s){
ST st;
InitStack(&st);
while(*s)
{
//1.若为左括号,则入栈
if((*s == '(')
|| (*s == '{')
|| (*s == '['))
{
Push(&st,*s);
++s;
}
else //2.若为有括号,则进行判断匹配
{
//若匹配到右括号后无左括号与之匹配,返回false
if(StackEmpty(&st))
{
DestoryStack(&st);
return false;
}
STDateType top = Top(&st);
Pop(&st);
if(top == '(' && *s != ')'
|| top == '{' && *s != '}'
|| top == '[' && *s != ']')
{
DestoryStack(&st);
return false;
}
else
++s;
}
}
//若在匹配完成后栈不为空,则表示还要括号没有完成匹配
if(!StackEmpty(&st))
return false;
DestoryStack(&st);
return true;
}
你是否发现若是我们要去使用一个栈的话不会忘了去往里面入数据或者是出数据,但是却时常会忘了【初始化】和【销毁】。这要如何是好呢
this指针
,只要是在成员函数内部都可以进行调用。而且还知晓了C++中原来是使用this指针接受调用对象地址的机制来减少对象地址的传入,减轻了调用者的工作。这也是C++区别于C很大的一点首先我们来整体地介绍一下这六个成员函数
【空类的概念】:如果一个类中什么成员都没有,简称为空类。
class Date{}
【默认成员函数概念】:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次
Init()
函数可以用来初始化年月日,而下面的Date()
就是一个构造函数,若是没有调用Init()函数进行初始化的话,这个构造函数就会被自动调用class Date
{
public:
void Init(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
void Print()
{
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
d1
和d2
,都会去自动调用构造函数,进行一个初始化操作,但是d2又去调用了Init()
函数,便对年月日再度进行了一次初始化。打印结果之后可以发现二者的成员变量不同需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
Init()
函数。接下去我们来聊聊它的各种特性,深入化对其进行一个了解void
,而是什么都不用写Date()
,这只是最普通的一种无参默认构造Date()
{
_year = 1;
_month = 1;
_day = 1;
}
Date(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
Date d1;
Date d2(2023, 3, 22);
Init()
函数了Date d1;
Date d2(2023, 3, 22);
Date d3();
d3()
这样去写就会产生歧义,编译器会认为这是一个函数的声明,Date
会被它当做是一一个返回值来看待,()
会被它当做是一个函数调用符。所以这点是要注意的谈到产生歧义这一块,再来给读者扩展一个知识点,既然讲到了【函数重载】,就来顺便说说【缺省参数】吧
//Date()
//{
// _year = 1;
// _month = 1;
// _day = 1;
//}
//Date(int y, int m, int d)
//{
// _year = y;
// _month = m;
// _day = d;
//}
Date(int y = 1, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
【存在多个默认构造函数】
int、char、double
)来说,不会做初始化处理;对自定义类型成员(class、struct、union
),对调用它的默认构造函数【⭐】
int、char、double..
甚至是指针;自定义类型就是class、struct、union..
对于默认生成的构造函数可能是没有设计好,对【自定义类型】会做初始化处理,但是对于【内置类型】不会做初始化处理所以为什么说C++那么难学,就是因为C++的语法错综复杂,很多初学者在看到这一幕之后就觉得很奇怪,去网上找问老师又没人给他说得通,于是就带着这个疑惑学下去了,后面也遇到类似的错误,还是这样模棱两可的,便从入门到放弃╮(╯▽╰)╭
Create
和Free
函数,可以看到分别调用了我们使用C语言实现的InitStack()
和DestroyStack()
来初始化和销毁【stIn】和【stOut】。如果你使用C语言做了这道题的话一定会感觉这很麻烦现在我们使用C++的类来试试MyQueue* myQueueCreate() {
MyQueue* qu = (MyQueue *)malloc(sizeof(MyQueue));
InitStack(&qu->stIn);
InitStack(&qu->stOut);
return qu;
}
void myQueueFree(MyQueue* obj) {
DestroyStack(&obj->stIn);
DestroyStack(&obj->stOut);
free(obj);
}
Stack
而言是我用C++实现的一个栈,它算做是一个内置类型,所以会使用默认的构造函数进行初始化【STL中可以直接用stack】class MyQueue {
public:
void push(int x) {
//..
}
//...
Stack _pushST;
Stack _popST;
};
不过你一定会想,内置类型不做初始化这一漏洞难道就这么让它放着吗?当然不会,在C++11中,对其进行了整改
那有同学问:若是成员变量是内置类型和自定义类型混搭呢?会发生什么?
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
_t
去调用了它的构造函数初始化了【year】、【month】、【day】 ,但是对于内置类型的成员却还是随机值,表明他们没有被初始化【总结一下】:
对于构造函数这一块其实还有些内容,像初始化列表、explicit关键字,文章内容过长,后续放链接
好,接下去我们就来讲讲类的第二大默认成员函数 —— 【析构函数】
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
【析构函数的概念】:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
int main()
{
Date d;
return 0;
}
~
即可~Date()
{
cout << "Date析构函数的调用" << endl;
_year = 0;
_month = 0;
_day = 0;
}
return
的时候按下F11】认识了什么是析构函数,接下去我们来看看有关析构函数的特性
~
bool isValid(char* s) {
Stack st;
while (*s)
{
//1.若为左括号,则入栈
if ((*s == '(')
|| (*s == '{')
|| (*s == '['))
{
st.Push(*s);
++s;
}
else //2.若为右括号,则进行判断匹配
{
//若匹配到右括号后无左括号与之匹配,返回false
if (st.Empty())
return false;
int top = st.Top();
st.Pop();
if (top == '(' && *s != ')'
|| top == '{' && *s != '}'
|| top == '[' && *s != ']')
{
return false;
}
else
++s;
}
}
//若在匹配完成后栈不为空,则表示还要括号没有完成匹配
return st.Empty();
}
Init()
函数进行手动初始化,接着很不同的一点就是这个DestroyStack()
,因为有默认析构函数的存在,所以不需要去显式地写一个析构函数,就方便了许多class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
调试结果如下:
接下去我就来详细解释一下这个析构的流程~
_year、_month、_day
以及一个自定义类型变量_t
。对于内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可_t
来说,属于Time的类所定义出来的对象,就要调用Time类的析构函数,但是在main函数中销毁Date类对象d的时候,是不可以直接调用Time类析构函数的,所以呢编译器会先去调用Date类的析构函数,不过Date类并没有显示地定义出一个析构函数,所以编译器会去调用【默认的析构函数】,目的是在其内部再去调用Time类的析构函数,即当Date对象销毁的时候,编译器要保证其内部的自定义对象可以正确销毁【总结一下】:
接下去我们来谈谈类中的第三个天选之子 —— 【拷贝构造函数】,提前预警⚠,本模块在理解上会比较困难,建议多看几遍
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
【拷贝构造函数概念】:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数也是特殊的成员函数,其特征如下:
//全缺省构造函数
Date(int y = 2000, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
//拷贝构造函数
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main(void)
{
Date d1;
Date d2(d1); //调用形式
return 0;
}
Date(Date d)
指的就是拷贝构造函数,Date d2(d1);
便是它的调用形式,用已经定义出来的对象d1来初始化d2&
就可以编过了,这是为什么呢?可能上面的这种形式过于复杂了,我先用下面这两个函数调用的形式来进行讲解
Func1(d)
来说叫做【传值调用】,对于Func2(d)
来说叫做【传引用调用】void Func1(Date d)
{
cout << "Func1函数的调用" << endl;
}
void Func2(Date& d2)
{
cout << "Func2函数的调用" << endl;
}
int main(void)
{
Date d;
Func1(d);
Func2(d);
return 0;
}
d
是d1
的拷贝;对于【传引用调用】不会产生拷贝,此时d
是d1
的别名;有同学说:这和我们之前学的不一样呀,之前都是像int
、char
这样的类型,难道规则也同样适用吗?
这里就又要说到【内置类型】和【自定义类型】的区别了,这个我们在上面讲构造和析构的时候也有提到过。对于内置类型的数据我们知道只有4/8
个字节,这个其实编译器直接去做一个拷贝就可以了;但是呢对于自定义类型的数据,编译器可没有那么大本事,需要我们自己去写一个拷贝构造函数,然后编译器会来调用
其实你也可以写成像下面这种形式,是一个传址形式进行调用,对于Date*
是一个对象指针,所以我们将其看做是内置类型
void Func3(Date* d)
{
cout << "Func2函数的调用" << endl;
}
Func3(&d1);
内置类型(包括指针)/ 引用传值
均是按照字节方式直接拷贝(值拷贝);对于自定义类型
,需要调用调用其拷贝构造函数完成拷贝但是在我这么对比分析之后,有位同学提出了下面的问题,这里来解答一下
为什么会存在拷贝构造?C语言中传递结构体也不需要拷贝呀?
内置类型会直接值拷贝,值拷贝是啥?
memcpy()
。不管你是int、double还是结构体,我都是一个个字节给你拷过去的所以,通过上面的一系列观察和总结,基本带读者了解了什么是拷贝构造、如何去调用拷贝构造,接下去我们来深入地研究一下拷贝构造
Date d2(d1)
需要实例化对象d2,所以要调用对应的构造函数,也就是拷贝构造函数,但是在调用拷贝构造函数之前要先传参,那刚才说了【自定义类型传参调用】就会引发拷贝构造,那调用拷贝构造就又需要传参数进来,传参数又会引发拷贝构造。。。于是就引发了这么一个无限递归的问题d
就是d1
的别名,那因为是d2去调用的拷贝构造,此时this指针所接收的便是d2
的地址,初始化的即为d2的成员变量Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date d2(d1);
不过对于上面这种拷贝构造的形式并不是很规范,一般的拷贝构造函数都写成下面这种形式
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
const
呢?这一点其实我们在模拟实现strcpy中其实也有说到过,有的时候你可能会不小心把代码写成下面这样Date(Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
const
加上后,编译器便报出了错误❌它还有第二点作用,我们再来看看
const
,此时这个对象就具有常属性,不可以被修改,然后此时再去使用d1对象初始化d2对象会发生什么呢?int main(void)
{
const Date d1;
Date d2(d1);
return 0;
}
const
做修饰之后,便可以做到【权限保持】,此时程序的安全性又增加了↑小结一下,对于const Date& d
这种不是做输出型参数,加上前面的const
的好处在于
① 防止误操作将原对象内容修改
② 防止传入const对象造成【权限放大】
对于构造、析构来说我们在上面讲到了若是自己不写的话都会去自动调用编译器默认生成的,那对于拷贝构造也同样适用吗?
//以下为有参构造
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
内置类型会处理,那自定义类型呢?也会处理吗?
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const Time&)" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
//构造..
//析构
private:
int _year;
int _month;
int _day;
Time _t; //内置自定义类型的成员
};
_t
,便要调用Time类的析构函数,但是要先调用编译器为Date类自动生成的析构函数,然后再去调用Time类的析构函数,此时自动生成的析构函数就派上了用场【忘记了再翻上去看看】_t
的时候就会去调用Time类的显式拷贝构造完成初始化工作因此对于像Date这种日期类来说,我们可以不用去自己去实现拷贝构造,编译器自动生成的就够用了,那其他类呢,像
Stack
这样的,我们继续来看看
st1
,往里面入栈了3个数据,接下去便通过st1去初始化st2
,通过上面的学习可以知道会去调用编译器自动生成的【拷贝构造】来完成,不过真的可以完成吗?我们来运行一下试试Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2(st1);
_array
就指向堆中的这块内存地址,接着s2去拷贝了s1,里面的数据是都拷贝过来了,但是s2的_array
也指向了堆中的这块空间push
了【1】、【2】、【3】,那么它的size就是3,但是s1与s2二者的size是独立的,不会影响,所以此时s2的size还是0,再去push
【4】、【5】、【6】的话还是会从0的位置开始插入,也这就造成了覆盖的情况不仅如此,二者指向同一块数据空间还会造成其他的问题
_array
都指向堆中的同一块空间,因此当s2去调用析构函数释放了这块空间后,那么s1对象的_array
就已经是一个野指针了,指向了堆中的一块随机地址,那再去对这块空间进行析构的话就会出现问题⚠那要如何去解决这个问题呢?此时就要涉及到【深拷贝】了
调用编译器自动为我们生成的拷贝构造函数去进行拷贝的时候会造成【浅拷贝】的问题,那什么又叫做深拷贝呢?
接下去我就来实现一下如何去进行【深拷贝】
Stack(const Stack& st)
{
//根据st的容量大小在堆区开辟出一块相同大小的空间
_array = (DataType *)malloc(sizeof(DataType) * st._capacity);
if (nullptr == _array)
{
perror("fail malloc");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType) * st._size); //将栈中的内容按字节一一拷贝过去
_size = st._size;
_capacity = st._capacity;
}
memcpy()
一一拷贝过来,此时两个对象中的_array
就指向了堆中两块不同空间,那么各自去进行入栈出栈的话就不会造成上述的问题了
但是这样自己去写拷贝构造感觉很麻烦诶,哪些类需要这样去深拷贝呢?
深刻理解了拷贝构造之后,我们再来看看产生拷贝构造的三种形式
Date d1;
Date d2(d1);
Date d3 = d2; //也会调用拷贝构造
void func(Date d) //形参是类的对象
{
d.Print();
}
int main(void)
{
Date d1;
func(d1); //传参引发拷贝构造
return 0;
}
func()
的形参是类的对象,此时在外界调用这个函数并传入对应的参数时,就会引发拷贝构造,通过调试观察一清二楚。而且当func函数执行结束时,内部的形参对象d
就会随着当前栈帧的销毁而去调用【析构函数】。到那时在外界为何又去调了一次析构呢?外界也就是main函数栈帧中的对象d1
,是作为实参进行传递的,出了main函数的栈帧当前也需要调用【析构函数】进行销毁Date func2()
{
Date d(2023, 3, 24);
return d;
}
int main(void)
{
Date d1 = func2();
d1.Print();
return 0;
}
对象d1
确实是以函数内部通过有参构造初始化完后的对象进行拷贝的再来看一个很经典的类MyQueue,我在前面说构造函数中自定义类型的时候也有讲到过
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
cout << "Stack()构造函数调用" << endl;
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void CheckCapacity()
{
if (_size == _capacity)
{
DataType* tmp = (DataType*)realloc(_array, sizeof(DataType) * _capacity * 2);
if (nullptr == tmp)
{
perror("fail realloc");
exit(-1);
}
_array = tmp;
_capacity = _capacity * 2;
}
}
void Push(const DataType& data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
Stack(const Stack& st)
{
cout << "Stack()拷贝构造函数调用" << endl;
//根据st的容量大小在堆区开辟出一块相同大小的空间
_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (nullptr == _array)
{
perror("fail malloc");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType) * st._size); //将栈中的内容按字节一一拷贝过去
_size = st._size;
_capacity = st._capacity;
}
~Stack()
{
cout << "~Stack()析构函数调用" << endl;
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
class MyQueue{
public:
//默认生成构造函数
//默认生成析构函数
//默认生成拷贝构造函数
private:
size_t _t = 1;
Stack _pushST;
Stack _popST;
};
讲了这么多有关拷贝构造的内容后,那它在现实的场景中有什么用呢?接下去我就带你来做一个【日期计算器】
首先来分析一下思路要如何去进行实现
> 当前月份的总天数
,就要产生进位,对于月份来说可以单独作为一块逻辑去进行进行实现。首先将加好后的天数减去当前月的天数,然后进位到下一个月,若是这个天数还大于当前月的天数,那又要发生进位,所以这段路逻辑可以放到一个循环中去实现,直到天数小于当前所在月的天数时,就停止进位。【一些细节部分的说明见代码】GetAfterXDay
,确定要传入的参数为多少天后的天数 ,返回值类型是一个Date类。然后在外界实例化出一个Date类的对象,调用有参构造进行初始化,通过这个对象去调用类中的日期计算函数,然后将其返回值给到一个日期类的对象做接收Date GetAfterXDay(int x)
Date d(2023, 3, 25);
Date d1 = d.GetAfterXDay(150);
_day
指得就是当前调用这个函数的日期实例类对象_day += x;
_day
是否超出了当前月的天数,但当前月的天数我们要先去计算出来,这里我又单独封装了一个函数int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[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; //2月闰年29日天
}
else
{
return monthArray[month];
}
}
_month++
,若是月份加上去之后超过12月了,此时年份就要产生进位,然后月份重新置为1_day -= GetMonthDay(_year, _month); //先把这个月的天数减去
_month++; //月份进位
if (_month == 13) //若是月份加到13月了
{
_year++; //年产生进位
_month = 1; //月份置为1
}
_day
的天数小于当前月的天数为止停止进位【this代表当前对象,可加可不加】while (_day > GetMonthDay(this->_year, this->_month))
this->
是白写的吗,就是为了告诉你修改的都是当前调用对象的成员变量,那么this
指向这个对象,*this
指的就是这个对象本身,return它就可以了return *this; //this指向这个对象,*this就是这个对象
接下去我们就用上面写的这段逻辑去试试是否可以运行
GetAfterXDay
计算天数后进行返回自身发生了修改,所以就导致在第二次计算的时候初始时间的不对Date tmp(*this)
得到了一份当前对象的临时拷贝,那么对于下面的所有操作,我们都修改【tmp】即可,最后也是将tmp进行返回,便可以获取到增加后的日期Date GetAfterXDay(int x)
{
Date tmp(*this); //做一份当前对象的临时拷贝
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) //若是加完后的天数 > 这个月的天数
{
tmp._day -= GetMonthDay(tmp._year, tmp._month); //先把这个月的天数减去
tmp._month++; //月份进位
if (tmp._month == 13) //若是月份加到13月了
{
tmp._year++; //年产生进位
tmp._month = 1; //月份置为1
}
}
return tmp; //返回临时修改的对象
}
return tmp
的时候。我们通过调试来进行观察你以为这样就完了吗,接下去我要拓展一些下个模块的知识 ——【赋值运算符重载】 ,不过只是一些雏形而已
+
和+=
这两个操作符,前者在运算之后不会改变,后者在运算之后会发生改变,也就是在自身做一个加法运算看了上面的这段话,再结合这个日期计算,你是否能想到我要讲什么呢?
+=
,调用对象自身会受到影响,但是后面;后面我们所做的优化修改就是+
,调用对象自身不会受到影响+
与+=
是一个道理//+ —— 自身不会改变
Date Add(int x){}
//+= —— 改变自身
Date AddEqual(int x){}
其实对于上面的【AddEqual】还可以去进行一个优化
Date
改成了Date&
,相信学得扎实的同学一定知道我为何去这样做,因为AddEqual返回的是*this
,也就是当前对象,那对于当前对象来说出了作用域是不会销毁的,那我们便可以使用引用返回去减少一次拷贝;但是呢对于Add来说返回的是tmp
,也就是我们通过拷贝得到的一个临时对象,出了作用域会销毁,此时不可以使用引用返回,否则很可能在外界接收的就是一个随机值Date& AddEqual(int x)
Date Add(int x)
更多的内容在【综合案例 —— Date日期类】的实现中还会细讲,继续看下去吧
Date类
class Date
{
public:
//3.全缺省构造函数
Date(int y = 2000, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d) //权限保持
{
//cout << "Date拷贝构造的调用" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//获取当前年这一月的天数
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
int monthArray[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; //2月闰年29日天
}
else
{
return monthArray[month];
}
}
//+ —— 自身不会改变
Date Add(int x)
{
Date tmp(*this); //做一份当前对象的临时拷贝
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) //若是加完后的天数 > 这个月的天数
{
tmp._day -= GetMonthDay(tmp._year, tmp._month); //先把这个月的天数减去
tmp._month++; //月份进位
if (tmp._month == 13) //若是月份加到13月了
{
tmp._year++; //年产生进位
tmp._month = 1; //月份置为1
}
}
return tmp; //返回临时修改的对象
}
//+= —— 改变自身
Date& AddEqual(int x)
{
_day += x;
while (_day > GetMonthDay(_year, _month)) //若是加完后的天数 > 这个月的天数
{
_day -= GetMonthDay(_year, _month); //先把这个月的天数减去
_month++; //月份进位
if (_month == 13) //若是月份加到13月了
{
_year++; //年产生进位
_month = 1; //月份置为1
}
}
return *this; //返回临时修改的对象
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
//cout << "Date析构函数的调用" << endl;
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
测试
void AddTest()
{
Date d(2023, 3, 25);
Date d2 = d.Add(100);
d.Print();
d2.Print();
}
void AddEqualTest()
{
Date d(2023, 3, 25);
Date d2 = d.AddEqual(100);
d.Print();
d2.Print();
}
int main(void)
{
//AddTest(); //25 + 100 = 125
AddEqualTest(); //25 += 100
return 0;
}
【总结一下】:
const
进行修饰,可以防止误操作和权限放大的问题拷贝构造这一块还存在编译器的优化,后续也以链接的形式附上
Date d1(2023, 3, 25);
Date d2(2024, 3, 25);
//等于==
bool Equal(const Date& d1, const Date& d2)
{
//...
}
//小于<
bool Less(const Date& d1, const Date& d2)
{
//...
}
//大于>
bool Greater(const Date& d1, const Date& d2)
{
//...
}
Equal(d1, d2);
Less(d1, d2);
Greater(d1, d2);
若是每个函数都是上面这样的命名风格,那么调用的人该多心烦呀╮(╯▽╰)╭
int、char、double
这些【内置类型】的数据,对于这些类型是语法定义的,语言本身就已经存在了的,都将它们写进指令里了基于上述的种种问题,C++为了增强代码的可读性引入了运算符重载
【概念】:运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
【函数名字】:关键字operator
后面接需要重载的运算符符号
【函数原型】:返回值类型 operator操作符(参数列表)
==
的运算符重载函数bool operator==(const Date& d1, const Date& d2)
注意事项:
接下去我便通过几点注意实现来带你进一步了解运算符重载
+
,内部实现了一个乘法运算,然后用当前对象的月数 * 10,最终改变了+
运算符的含义,这种语法虽然是可以编译过的,但是写出来毫无意义,读者可以了解一下运算符重载可以放在全局,但是不能访问当前类的私有成员变量
解决办法1:去掉[private]
,把成员函数全部设置为公有[public]
解决办法2:提供共有函数getYear()
、getMonth()
、getDay()
解决办法3:设置友元【不好,会破坏类的完整性】
解决办法4:直接把运算符重载放到类内
bool
类型,将其输出一下看看<<
这个操作符似乎出现了什么问题,这就是因为<<
的优先级比==
来得高,所以会优先执行cout << d1
,那么中间的==
就不知道和谁结合了,因此出了问题。所以在运算符重载之后我们还要考虑操作符的优先级问题d1 == d2
的外面加上()
即可,让他们俩先进行运算this
,用于接收调用对象所传入的地址,上面我们在写【日期计算器】的是有也有在类内使用过这个this
指针==
来说是个【双目操作符】,其运算符只能有两个,那此时再加上隐藏形参this
的话就会出现问题bool operator==(Date* this, const Date& d1, const Date& d2)
this
指针所指对象与形参中传入的对象bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
对象
.的形式去调用,运行结果如下d1 == d2
因为有了运算符重载,所以当编译器执行到这个语句的时候,就会通过call
这个重载函数的地址,然后类内的成员函数执行。我们可以通过【反汇编】来进行查看.*
、 ::
、 sizeof
、?:
、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。【运算符重载】:自定义类型对象可以使用运算符
【函数重载】:支持函数名相同,参数不同的函数,同时可以用
上面教了要如何去写
==
的运算符重载,接下去我们就来对其他运算符写一个重载
<
,读者可以试着自己在编译器中写写看bool operator<(const Date& d)
接下去展示一下三位同学的代码
//Student1
bool operator<(const Date& d)
{
return _year < d._year
|| _month < d._month
|| _day < d._day;
}
//Student2
bool operator<(const Date& d)
{
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
//Student3
bool operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
首先来看一下第一位同学的运行结果
2025.2.35
> 2024.3.25
,但是这位同学的代码逻辑却出现了问题,我们对照代码来分析一下||
来说,有1个为真即为真,全假才为假,所以最后的结果才返回了true//Student1
bool operator<(const Date& d)
{
return _year < d._year
|| _month < d._month
|| _day < d._day;
}
接下去再来看看第二位同学的运行结果
&&
的符号,表示二者都满足才可以;那对于天来说也是一样的,要满足年和月相等的条件下才可以比较大小//Student2
bool operator<(const Date& d)
{
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
但是我却采用了第三位同学的代码来作为标准答案
||
改成了if…else条件判断罢了,原理都是一样的。//Student3
bool operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
说点题外话
你觉得自己正处于那种状态呢?
好,我们回归正题,继续来讲运算符重载
==
、<
如何去进行重载,那小于等于呢?该如何去实现?bool operator<=(const Date& d)
有同学说:这简单,把<
都改成<=
不就好了
bool operator<(const Date& d)
{
if (_year <= d._year) {
return true;
}
else if (_year == d._year && _month <= d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day <= d._day) {
return true;
}
else {
return false;
}
}
return (*this < d) || (*this == d);
<
和==
,this
指向当前对象,那么*this
指的就是当前对象,这样来看的话其实就一目了然了小于、小于等于都会了,那大于>
和大于等于>=
呢?不等于!=
呢?
bool operator>(const Date& d)
bool operator>=(const Date& d)
bool operator!=(const Date& d)
//大于>
bool operator>(const Date& d)
{
return !(*this <= d);
}
//大于等于>=
bool operator>=(const Date& d)
{
return !(*this < d);
}
//不等于!=
bool operator!=(const Date& d)
{
return !(*this == d);
}
这里就不给出测试结果了,读者可自己修改日期去查看一下
class Date
{
public:
//3.全缺省构造函数
Date(int y = 2000, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
Date(const Date& d) //权限保持
{
//cout << "Date拷贝构造的调用" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//等于==
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//小于<
bool operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
//小于等于<=
bool operator<=(const Date& d)
{
return (*this < d) || (*this == d);
}
//大于>
bool operator>(const Date& d)
{
return !(*this <= d);
}
//大于等于>=
bool operator>=(const Date& d)
{
return !(*this < d);
}
//不等于!=
bool operator!=(const Date& d)
{
return !(*this == d);
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
//cout << "Date析构函数的调用" << endl;
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
有了运算符重载的概念后,我们就来讲讲什么是赋值运算符重载
首先给出代码,然后我再一一分解叙述
Date& operator=(const Date& d)
{
if (this != &d) //判断一下是否有给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
【参数类型】:const T&,传递引用可以提高传参效率
&
是为了减少传值调用而引发的拷贝构造,加const
则是为了防止当前对象被修改和权限访问的问题,如果忘记了可以再翻上去看看【返回*this】 :要复合连续赋值的含义
Date d1(2023, 3, 27);
Date d2;
Date d3;
d3 = d2 = d1;
*this
【返回值类型】:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
【多方位考虑】:检测是否自己给自己赋值
d1 = d1;
this指针
所指向的地址和传入的地址一致的话,就不用做任何事,直接返回当前对象即可。若是不同的话才去执行一个赋值的逻辑if (this != &d)
知晓了基本写法和注意事项后,我们就来测试运行一下看看是否真的可以完成自定义类型的赋值
=
去进行日期之间的赋值d2 = d1; //赋值重载
Date d3 = d2; //赋值重载?
d2 = d1
就是我们刚才说的赋值重载,去类中调用了对应的函数;但是对于Date d3 = d2
来说,却没有去调用赋值重载,而是去调用了【拷贝构造】,此时就会有同学很疑惑?
这里一定要区分的一点是,赋值重载是两个已经初始化的对象才可以去做的工作;对于拷贝构造来说是拿一个已经实例化的对象去初始化另一个对象
d1
和d2
是两个已经初始化完的对象,但是d3还未初始化,还记得我在将拷贝构造的时候说过的这一种形式吗,如果忘了就再翻上去看看吧重点地再来谈谈默认的赋值运算符重载,相信在看了构造、析构、拷贝构造后,本小节对你来说不是什么难事
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int y = 2000, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
d1
初始化d2
的时候,去调用了Time类的拷贝构造,这是为什么呢?我们其实可以先来看看Date类中的成员变量有:_year、_month、_day以及一个Time类的对象_t
,通过上面的学习我们可以知道对于前三者来说都叫做【内置类型】,对于后者来说都叫做【自定义类型】那我还是一样会提出疑问,既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
没错,也是我们的老朋友Stack类。我想你可能已经猜到了结果
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
_array
都指向了一块独立的空间,但是在赋值之后,s1和s2的_array
还是指向了同一块空间。此时便会造成两个问题
_array
所申请出来的空间没有释放会导致内存泄漏中间穿插一点内容,就是有关这个赋值运算符所定义的位置
operator=
也就是赋值重载只能写在类内,不可以写在类外,但一定有同学还是会疑惑为什么要这样规定,且听我娓娓道来~Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
拿一些权威性的东西来看看,以下是《C++ primer》中的原话
operator
,使用这个关键字再配合一些运算符封装成为一个函数,便可以实现对自定义类型的种种运算,也加深巩固了我们对前面所学知识的掌握=
进行重载,分析了一些它的有关语法使用特性以及注意事项,也通过调试观察到了它原来和默认拷贝构造的调用机制是一样的,毕竟大家同为天选之子。但是也要区分二者的使用场景,不要混淆了接下去中间穿插讲一个const成员函数,这个实际的开发中可能会用到,也为下面的const取地址操作符重载做铺垫
【概念】:将const
修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
class A {
public:
void Print()
{
cout << _a << endl;
}
private:
int _a = 10;
};
int main(void)
{
A a;
a.Print();
return 0;
}
const
做修饰,编译却报出了错误说了一些有关this指针的内容,那有些同学就看不太懂,这是为啥呀???this
指针有关的报错,那我们是否可以从这个点入手去分析一下呢?要知道一个对象去调用当前类中的成员函数时会传递它的地址给到成员函数中的隐藏形参this指针,然后在内部this指针就可以通过不同的对象地址去访问不同地址空间中对应的成员变量void Print(A* this)
那有同学问:这该怎么办呀,那可以不要让它放大吗?给this指针也加个const,这样就可以权限保持了吧
小王:那这不是没办法了吗,老师?出现Bug了
const
,那这个成员函数就是一个const成员函数了void Print() const
{
cout << _a << endl;
}
小东:但是为什么加这么一个const就可以做到为this指针做修饰呢?编译器到底是怎么识别的?
const
做修饰之后,this指针就变成了一个常量指针,对于常量指针而言是不可以去修改它所指向内容的,那_a += 1
这个操作就是违法的;而且this指针本身就是一个指针常量,那此时它完全就被锁住了,就像下面这样void Print(const A* const this)
当我放出这样的东西后,又迎来了一堆的问题。。。。
小明:那限制成这样了外面要传个普通的不具有常性的成员是不是都不可以了?
小李:const只能用在成员函数上吗?我去修饰普通函数可不可以呢?
const
是用来修饰this指针的,那谁有this指针呢?是不是成员变量才有这个隐藏的参数呀,是吧。类外的普通函数是没有this指针滴
小叶:const修饰成员函数只能放在右边吗?能不能放在左边?
当同学们提问完之后,就轮到我问了,我便提出了一下四个问题,读者在学习完后可以试着解答一下
问题:
const对象可以调用非const成员函数吗?
非const对象可以调用const成员函数吗?
const成员函数内可以调用其它的非const成员函数吗?
非const成员函数内可以调用其它的const成员函数吗?
答案:
A* const
,即当前对象可修改也可不修改这两个天选之子呢类中也是会默认提供的,但是呢我们用得却不多
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
return this
,那返回的也就是当前对象的地址
说实话,这个在平常的开发中真用的不多,甚至用不到,这里提一下读者了解一下就可以了。你要真的想去玩一玩这个东西的话倒也可以
学习完了类的六大天选之子,接下去我们来练习一道综合案例 —— Date日期类
这个案例我打算通过多文件的形式去进行编写,分为:Date.h
、Date.cpp
、test.cpp
[Date.h]
中的内容class Date
{
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);
public:
//获取当月的天数
int GetMonthDay(int year, int month);
//构造函数
Date(int y = 2000, int m = 1, int d = 1);
//拷贝构造函数
Date(const Date& d);
//赋值重载
Date& operator=(const Date& d);
//等于==
bool operator==(const Date& d);
//不等于!=
bool operator!=(const Date& d);
//小于<
bool operator<(const Date& d);
//小于等于==
bool operator<=(const Date& d);
//大于>
bool operator>(const Date& d);
//大于等于>=
bool operator>=(const Date& d);
//前置++
Date& operator++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);
//日期 += 天数
Date& operator+=(int days);
//日期 + 天数
Date operator+(int days);
//日期 -= 天数
Date& operator-=(int days);
//日期 - 天数
Date operator-(int days);
//日期 - 日期
int operator-(const Date& d); //构成重载
//打印
void Print();
//析构函数
~Date();
private:
int _year;
int _month;
int _day;
};
==
、!=
、>
、>=
、<
、<=
<<
和流提取>>
对日期进行输入输出注意:下面四个模块的编写都是放在
Date.cpp
中,因此成员函数前后都要加上域作用限定符::
① 全缺省构造函数
.h
的头文件中给出,而不可以写在.cpp
中,因为主文件要包含的头文件是Date.h
Date(int y = 2000, int m = 1, int d = 1);
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
但是这样就好了吗?我们首先来main函数中测试一下
因此当外界在实例化对象调用构造函数的时候,我们应该要去进行一个日期是否合法的判断
assert()
assert(month > 0 && month < 13);
GetMonthDay
,具体实现细节这里便不再多说int Date::GetMonthDay(int year, int month)
{
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int days = monthArray[month];
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
return days + 1;
}
else {
return days;
}
}
day
去进行一个比较,若是合法的话就对当前对象的年、月、日去做一个初始化的工作,若是不合法的话就输出一句话进行提醒//判断一下对象实例化的年月日是否合法
int days = GetMonthDay(year, month);
if (day > 0 && day <= days) {
_year = year;
_month = month;
_day = day;
}
else {
cout << "所传入日期不合法" << endl;
}
② 拷贝构造函数
&
减少拷贝、提高效率,以及const
常修饰对象防止误操作和权限方法即可Date::Date(const Date& d) //权限保持
{
cout << "Date拷贝构造的调用" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
③ 赋值运算符重载
*this
来说出了作用域不会销毁,因此我们可以使用引用返回Date&
来减少临时拷贝的过程Date Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
④ 析构函数
Date::~Date()
{
cout << "Date析构函数的调用" << endl;
_year = 0;
_month = 0;
_day = 0;
}
这里要说明一点,怕有的同学混淆,对于日期类来说器成员变量都是【内置类型】的,不会出现深拷贝之类的问题,所以构造、拷贝构造、赋值重载、析构都是可以不用去实现的,我这里实现只是为了起到知识点巩固的效果
接下去我们进入第二模块,来实现一下关系运算符的重载
① 等于 ==
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
② 不等于 !=
==
即可bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
③ 小于 <
bool Date::operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
④ 小于等于 <=
<
和==
即可bool Date::operator<=(const Date& d)
{
return (*this < d) || (*this == d);
}
⑤ 大于 >
<=
的对立面bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
⑥ 大于 >=
<
的对立面bool Date::operator>(const Date& d)
{
return !(*this < d);
}
好,接下去我们来进行第三模块函数的编写,这一块可能会有点难,做好准备,上车了
① 前置++
*this
即可,最后要给到++之后的返回值,那便返回*this
即可,那返回一个出了作用域不会销毁的成员,我们可以使用引用返回减少临时拷贝Date& Date::operator++()
{
*this += 1;
return *this; //返回++之后的值
}
② 后置++
*this
的临时拷贝,当自身的值递增之后,返回之前临时拷贝的tmp即可。不过要注意的是,tmp出了作用域不会销毁,因此不可以使用【引用返回】Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp; //返回++之前的值
}
来测试一下
这里我来解释一下后置++函数内的
int
是什么意思
d1.operator++(0)
,里面填充一个int类型的数据,无论是0或者1 都可以看完了前置++和后置++,那么前置–和后置–也不会有什么问题
③ 前置- -
Date& Date::operator--()
{
*this -= 1;
return *this; //返回--之后的值
}
④ 后置- -
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp; //返回--之前的值
}
++
或者是--
都需要现有一份临时拷贝,但是前置++
或者是--
。这就是为什么我们C语言在写循环条件的递增时要用++i
而不是i++
的原因了上面的
++
、--
都是一个单位的变化,那是否可以多变化一些呢,例如自己传入想要增加或减少的天数
⑤ 日期 += 天数
Date& Date::operator+=(int days)
+=
是对当前调用对象自身的变化,所以直接在_day
在进行操作即可,当加上days后去判断一下是否大于当前月的天数【判断的逻辑上面讲过了,GetMonthDay
可以复用】,如果是的话就要先减去这个月的天数,然后将月份进行递增。但月份在递增的过程中也会超出一个临界值,若是_month > 12
那我们就要月份改为来年的1月,然后将年份进行递增。一直循环往复前面的操作,知道_day
比当前月的天数来的小或者相等为止,因为是对调用对象自身进行操作,所以return *this
即可,出了当前作用域不会销毁,那再考虑到使用【引用返回】Date& Date::operator+=(int days)
{
_day += days;
while (_day > GetMonthDay(_year, _month)) {
//把当月的天数先减掉
_day -= GetMonthDay(_year, _month);
++_month;
if (_month > 12) {
//将月份置为来年的1月
_month = 1;
++_year;
}
}
return *this;
}
⑥ 日期 + 天数
+=
实现了之后,+
就可以去进行一个复用了,不需要再将上面的这块逻辑再写一遍。我们都知道+
之后不会改变自身的大小,所以不能对当前对象本身去进行一个操作,而是要做一份临时拷贝去加这个天数。又因为是临时拷贝,所以不可以使用【引用返回】Date Date::operator+(int days)
{
Date tmp(*this); //做一份当前对象的临时拷贝
tmp._day += days;
return tmp; //+不会改变自身,返回临时对象【出了作用域销毁,不能引用返回】
}
来测试一下
+=
和+
会写了之后,我们来看看一个日期要怎么减去对应的天数
⑦ 日期 -= 天数
_day
减去传入的days。那若这个天数还是正的话就不会有问题,但减完之后这个天数为负了,就要去考虑月份的进制了_day -= days;
_day <= 0
时我们进入这段逻辑,因为一个日期是没有0日的。那要怎么去处理这个天数呢,就要去上个月借,若是上个月不够了再向它的上个月借。那要这样一直借的话我们就先要去把月处理干净了才可以。但是月份在递减的过程中可能会减到0月,此时就不对了,这和+=
到达13月是一个道理,首先将年份减1,然后将月份改为上一年的12月即可_day
是一个负数,怎么让它变成一个正数呢,就是加上这个月的天数即可。一直这么循环往复,直到_day > 0
为止while (_day <= 0)
{
//先把月处理干净
_month--;
if (_month == 0) { //如果月减到0,说明出问题了,轮回到上一年
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
-=
,改变的是当前对象自身,所以需要return *this
,还是一样,返回一个出了作用域不会销毁的对象,可以使用【引用返回】return *this;
⑧ 日期 - 天数
-
也是一样,复用一下-=
即可Date Date::operator-(int days)
{
Date tmp(*this);
tmp._day -= days;
return tmp;
}
来测试一下
不过对于上面的
+=
、+
、-=
、-
完全没有任何纰漏了吗?如果我去加上一个负数呢?
Date d1(2023, 3, 1);
Date d2 = (d1 += -20);
operator+=
的days是一个负数,那么当前对象的_day
在累加后还是一个负数,此时根本都不会进入到下面的while循环中,因此出现了问题那该如何去解决呢?
days < 0
的话,此时去复用一下-=
即可,那么逻辑就转变为【加上一个负数等于减去一个正数】,所以要在days
前加上一个负号就可以起到负负得正的效果if (days < 0)
{
*this -= -days;
return *this;
}
通过调试来看看Bug修正后的结果
+=
存在这样的问题,其实-=
也是一样的if (days < 0)
{
*this += -days;
return *this;
}
这里就不做调试观察分析了,展示一下运行结果
日期除了可以去加减固定的天数之外,两个日期之间还可以去进行运算,这个之前。有提到过,对于日期 + 日期来说是没有意义,但是日期 - 日期还是存在一定意义的
⑨ 日期 - 日期
对于两个日期的差有很多不同的计算方法,这里我介绍一种比较优的思路
*this
是为较大的日期,而形参中的d是小的那个日期。然后定义一个flag
做标记值小于<
运算符了,再去比较一下两个对象谁大谁小,然后做一个更新,若是当前对象来的小的话,flag就要置为-1,因为二者相减的话会变成一个负数,此时就可以用到这个标记值了!=
运算符,让小的日期不断++,直到和大的日期相同为止return flag * n
即可,二者之间相差的是负数天还是正数天一目了然下面是代码
int Date::operator-(const Date& d) //构成重载
{
Date max = *this;
Date min = d;
int flag = 1;
//再进行比较算出二者中较大的那一个
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
//计算二者之间相差几天
int n = 0;
while (min != max)
{
min++;
n++;
}
return flag * n;
}
重载好后对两个日期去进行相减就发现确实是正确的【离下一个清明节放假还剩7天】
好,最后一个模块,我们来讲讲流插入和流提取
Print()
函数打印观察日期的变化情况,虽然调用一下也不用耗费多少时间,但总觉得还是有些麻烦了,如果可以像正常变量一样直接用cout << a
输出该多好cout
究竟是个什么东西。从中其实看到了一个很熟悉的东西叫做
,这是我们日常在写代码的时候都会包的头文件,而从中又包含了
和
,对于【cin】来说它是属于输入流的,对于【cout】来说是属于输出流的一种using namespace std
,我们可以进到这个命名空间中看看
小王:但为什么像我们平常写的cout << a
、cout << b
不会有问题呢?
int a = 10;
int b = 20;
cout << a;
cout << b;
int
、char
、double
来说都内置类型,我们前面重载的这些运算符就是为了去和自定义类型结合,基本每个运算符如果要去使用的话都要进行重载,只有像默认的【赋值重载】和【取地址重载】不需要之外那其实这就很清楚了,在官方的库中,已经为我们重载好了相应的运算符,所以内置类型的数据才可以直接去进行输出;而它们为什么可以同时存在呢?原因就在于这些函数又发生了一个重载,如果不清楚规则的话看看C++运算符重载规则
小李:那我们若是要去输出自定义类型的数据时,是不是也可以使用运算符重载呢?这个也可以重载吗?
<<
在C语言中我们有学习过是【按位左移】的意思,只是在C++中它被赋予了新的含义而已,那此时我们就可以在类中重载一下这个运算符了,然后输出对应的日期void Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
}
但编译器还是报出了一些错误,那有的同学就感到很疑惑了??
小叶:哦,那这样的话cout
就是第二个参数了,那我用cout.operator<<(d1)
去调用不就好了吗?
ostream
这个类里面去,和内置类型的重载放在一起,但是你可以去修改官方的库吗?很明显不能╮(╯▽╰)╭ostream&
参数了,不要当前所调用对象成为第一个参数那只有一个办法,那就是不要把这个函数写在写在类里面了!!!小叶:嗯,也是。那把它写到类外来吧,作为全局的一个函数
public
共有【不推荐】GetYear()
、GetMonth()
、GetDay()
【Java中常见】friend
做为修饰,就可在类内声明一下这个函数为当前类的友元函数friend void operator<<(ostream& out, const Date& d);
cout << d1
就可以发现可以正常运行了cout << d1 << d2;
ostream
的对象才可以,那也就是把这个【out】返回即可,便可以实现多次流插入了ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
既然流插入可以实现了,那还有一个双胞胎兄弟叫做【流提取】,也就是我们常用的输入
>>
ostream
转换为istream
输入流即可friend istream& operator>>(istream& in, Date& d);
const
来进行又是,不然它就具有常属性,我们无法往里面去写入东西istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
来测试一下
其实对于上面的【流提取】和【流插入】还可以再去进行一个优化,那就是将其定义为【内联函数】
.h
的头文件中,因为只有声明没有链接的时候才要去找,直接就有定义不用链接,在编译阶段就能拿到地址,然后call它的地址就可以进到那个函数里了所以对于内联函数来说不可以写成下面这种形式【只在.h
声明】,这样它就找不到函数的定义了
正确的应该像下面这样,将函数的定义与声明写在一起
这里再提一个小知识点,其实我在类和对象的封装思想中有讲到了,就是对于类中直接定义的成员函数会被直接当成是【内联函数】看待,这里就不能演示了,因为我们就是从类中将这个两个函数抽离出来的,为了可以
cin >> d1 >> d2
和cout << d1 << d2
例如像下面的这些,都是可以声明为const成员函数的【注意在定义部分也要加上】
//获取当月的天数
int GetMonthDay(int year, int month); 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;
//大于等于>=
bool operator>=(const Date& d) const;
//日期 + 天数
Date operator+(int days) const;
//日期 - 天数
Date operator-(int days) const;
//日期 - 日期
int operator-(const Date& d) const; //构成重载
Date.h
#pragma once
#include
#include
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
//获取当月的天数
int GetMonthDay(int year, int month) const;
//构造函数
Date(int y = 2000, int m = 1, int d = 1);
//拷贝构造函数
Date(const Date& d);
//赋值重载
Date& operator=(const Date& d);
//等于==
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++();
//后置++
Date operator++(int);
//前置--
Date& operator--();
//后置--
Date operator--(int);
//日期 += 天数
Date& operator+=(int days);
//日期 + 天数
Date operator+(int days) const;
//日期 -= 天数
Date& operator-=(int days);
//日期 - 天数
Date operator-(int days) const;
//日期 - 日期
int operator-(const Date& d) const; //构成重载
//打印
void Print() const;
//析构函数
~Date();
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
//获取当月的天数
int Date::GetMonthDay(int year, int month) const
{
int monthArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int days = monthArray[month];
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
return days + 1;
}
else {
return days;
}
}
//3.全缺省构造函数
Date::Date(int year, int month, int day)
{
assert(month > 0 && month < 13);
//cout << "Date构造的调用" << endl;
//判断一下对象实例化的年月日是否合法
int days = GetMonthDay(year, month);
if (day > 0 && day <= days) {
_year = year;
_month = month;
_day = day;
}
else {
cout << "所传入日期不合法" << endl;
}
}
//拷贝构造
Date::Date(const Date& d) //权限保持
{
//cout << "Date拷贝构造的调用" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值重载
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//等于==
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//不等于!=
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
//小于<
bool Date::operator<(const Date& d) const
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
//小于等于==
bool Date::operator<=(const Date& d) const
{
return (*this < d) || (*this == d);
}
//大于>
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
//大于等于>=
bool Date::operator>=(const Date& d) const
{
return !(*this < d);
}
//前置++
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; //返回--之前的值
}
//日期 += 天数
Date& Date::operator+=(int days)
{
if (days < 0)
{
*this -= -days;
return *this;
}
_day += days;
while (_day > GetMonthDay(_year, _month)) {
//把当月的天数先减掉
_day -= GetMonthDay(_year, _month);
++_month;
if (_month > 12) {
//将月份置为来年的1月
_month = 1;
++_year;
}
}
return *this;
}
//日期 + 天数
Date Date::operator+(int days) const
{
Date tmp(*this); //做一份当前对象的临时拷贝
tmp._day += days;
return tmp; //+不会改变自身,返回临时对象【出了作用域销毁,不能引用返回】
}
//日期 -= 天数
Date& Date::operator-=(int days)
{
if (days < 0)
{
*this += -days;
return *this;
}
_day -= days;
while (_day <= 0)
{
//先把月处理干净
_month--;
if (_month == 0) { //如果月减到0,说明出问题了,轮回到上一年
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//日期 - 天数
Date Date::operator-(int days) const
{
Date tmp(*this);
tmp._day -= days;
return tmp;
}
//日期 - 日期
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 n = 0;
while (min != max)
{
min++;
n++;
}
return flag * n;
}
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
Date::~Date()
{
//cout << "Date析构函数的调用" << endl;
_year = 0;
_month = 0;
_day = 0;
}
test.cpp
int main(void)
{
//Date d1(2023, 3, 29);
//Date d2(2023, 4, 5);
Date d1;
Date d2;
cin >> d1 >> d2;
cout << endl;
cout << d1 << d2;
return 0;
}
好,最后来总结一下本文所学习的内容
const
去修饰当前对象的this指针,使其变为常量指针,使得在该成员函数中不能对类的任何成员进行修改以上就是本文要介绍的所有内容,感谢您的阅读,如果觉得还可以就三连支持一下吧,完结撒花✿✿ヽ(°▽°)ノ✿