谈对象第二弹: C++类和对象(中)
文章目录
- 一、类的默认成员函数
- 二、构造函数
- 三、析构函数
- 四、拷贝构造函数
- 五、运算符重载
- 5.1运算符重载
- 5.2赋值运算符重载
- 5.3实现日期类
- <<、>>重载
- 检查、获取天数
- 关系运算符重载
- 算数、赋值运算符重载
- Date.h
- Date.cpp
- 六、取地址运算符重载
- 6.1const成员函数
- 6.2取地址运算符重载
一、类的默认成员函数
默认成员函数是用户不写,编译器会自动生成的成员函数,称为默认成员函数。在一个类中,我们不显示实现,编译器会自动实现的默认成员函数有6个,最重要的是前4个:构造函数、析构函数、拷贝构造函数、赋值运算符重载。后2个取地址运算符重载不重要。C++11还增加了两个默认成员函数:移动构造和移动赋值。
- 我们不写,编译器自动生成的函数是否满足使用需求?
- 不满足使用需求,自己该如何实现?
二、构造函数
构造函数是一个特殊的成员函数,其主要任务是在对象实例化时,自动调用构造函数,这种功能完美代替了手动实现Init函数。
构造函数的框架:
- 函数名与类名相同
- 无返回值(没有返回值,也不需要要给 void)
- 对象实例化时系统会自动调用构造函数
- 构造函数可以重载
- 通过给参数或对参数给缺省值实现重载
class Date
{
public:
Date()
{
_year = 2024;
_month = 9;
_day = 9;
}
Date(int year = 2024, int month = 8, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
通过调试功能不难发现,在创建一个对象后,编译器自动调用了默认构造函数,完成了对d1对象的初始化。
-
若我们没有显示实现构造函数编译器会自动生成默认构造函数
-
默认构造函数:并非只有编译器自动生成的构造函数称为默认构造函数,除此之外还有全缺省构造函数,无参构造函数,总的来说不需要传递参数的构造函数,属于默认构造函数。而无参构造函数和全缺省构造函数虽然构成函数重载,但它们同时实现时在调用上存在歧义,所以这3个默认构造函数有且只能存在一个。
编译器会自动调用的构造函数称为默认构造函数,需要传递参数的构造函数不属于默认构造函数。
还没有运行编译器就开始报错,Date类没有默认构造函数可用。同上,半缺省的构造函数也不属于默认构造函数
错误调用构造函数:
int main()
{
Date d1();//存在歧义的构造函数,是函数声明、还是函数定于
// warning C4930: “Date d1(void)”: 未调用原型函数(是否是有意用变量定义的?)
return 0;
}
- 编译器对变量类型进行分类,
int、double、float、char、指针
等属于内置类型,而使用struct,class
实现的类型属于自定义类型。 - 我们不写编译器自动生成的默认构造函数编译器对内置类型没有需求,初始化的结果是不确定的看编译器
而自定义类型,则会自动调用它的默认构造函数,若这个自定义类型没有默认构造函数编译器也会不生成,编译器会报错,需要初始化这个成员变量则需要使用初始化列表(下篇介绍)。
#include <iostream>
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
}
private:
int* _a;
int _top;
int _capacity;
};
class Myqueue
{
public:
Myqueue()
{
cout << "Myqueue()" << endl;
}
private:
Stack pushst;
Stack popst;
};
int main()
{
Myqueue mq;
return 0;
}
第一种调用行为,先调用Myqueue的默认构造在去调用Stack对应的构造函数。
第二种调用行为,不实现Myqueue的默认构造函数,编译器会去调用自定义类型的构造函数。
总的来说,编译器对于自定义类型在初始化是总会去调用它的默认构造。
三、析构函数
析构函数完成与构造函数的功能相反,析构函数本身不需要对对象进行销毁,在结束一个局部函数的调用后,C++规定对象在销毁时会自动调用它对应的析构函数完成资源清理,而函数存在栈帧,函数结束后栈帧销毁,空间就被释放了不需要我们去管理。析构函数的功能可以类比Stack的Destory,而日期类严格来说不需要析构函数。析构函数实现的功能不是销毁,而是对资源的清理。
析构函数一般是这样玩,但有特殊需求也可以用析构
析构函数的特点:
-
析构函数名在类名前加上字符
~
-
无参数、无返回值
-
~Date() { }
-
-
对象生命周期结束时,系统会自动调用析构函数。
#include <iostream>
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
这里创建了对象d1,当main函数结束时,通过运行结果可以发现编译器自动调用了d1的析构函数。
- 一个局部域有多个对象,C++规定后定义的先析构
int main()
{
Date d1;
Stack st;
return 0;
}
这里定义了两个对象d1和st,根据编译器的运行结果不难印证后定义的先析构的规定。多个对象也不仅指不同类型之间的对象,同种类型之间的对象也会出现这种情况。
、
- 一个类只有一个析构函数。若没有显示定义,系统会自动生成默认的析构函数。与构造函数类似,我们不写编译器会自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数。
#include <iostream>
class Date
{
public:
Date(int year = 2024, int month = 8, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new int[capacity];
_top = 0;
_capacity = capacity;
}
void Push(int x)
{
//………………
_a[_top++] = x;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[_capacity] _a;
_a = nullpr;
_capacity = 0;
_top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Date d1;
Stack st;
st.Push(1);
st.Push(2);
return 0;
}
基于上述实现的栈类和日期类,运行代码,当main函数结束时若我们没有显示实现日期类的析构函数,通过vs编译器的调试功能不难发现编译器实现的析构函数没有对日期类进行改动,日期大小也还存在;而对于栈类,我们没有显示实现它的析构函数,Visual Studio 2022版本的编译器,从表面上也没有对栈类进行资源清理。
显示实现栈类和日期类的析构函数时,当main函数结束时,编译器自动调用了栈类和日期类的析构函数,完成资源清理。
- 显示写析构函数,对于自定义类型成员也会调用它的析构函数,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
下列代码中栈的代码上述已经实现,避免冗余没有重复实现。
#include <iostream>
class Myqueue
{
public:
Myqueue()
{
//cout << "Myqueue()" << endl;
}
~Myqueue()
{
cout << "~Myqueue()" << endl;
}
private:
Stack pushst;
Stack popst;
};
int main()
{
Myqueue mq;
return 0;
}
形如Myqueue的类,成员变量都是自定义类型,我们不去实现Myqueue的析构函数,编译器会自动去调用栈类的析构函数,若显示实现了Myqueue的析构函数编译器还是会去调用栈类的析构函数总的来说自定义类型成员无论什么情况都会自动调用析构函数,前提是将它对应的析构函数写上,避免造成资源泄露。
- 如果类中没有申请资源,析构函数可以不用写,直接使用编译器生成的默认析构函数,如日期类;如果默认生成的析构被调用,那也不需要显示写析构函数如 MyQueue,两个栈实现队列;但是有资源申请时,一定要写析构函数,否则会造成资源泄露,如栈Stack。
- 日期类一般不需要写析构,它在堆区没有开辟空间存放资源,而栈就不一样,他在堆区是有资源存放的,它是必须调用析构函数释放销毁。最重要的就是这个类型指向有资源需要调用析构函数。
最后举了,括号匹配问题的例子,来区分体会构造函数和析构函数,与最早用C语言实现的括号匹配的差异
四、拷贝构造函数
构造函数的第一个参数是自身类类型的引用,且其余的参数都有缺省值,则此构造函数有称为拷贝构造函数,拷贝构造函数是一种特殊的构造函数,它完成对像与没有被实例化对象之间的拷贝。
int main()
{
Date d1;
Date d2 = d1;
return 0;
}
拷贝构造函数特性:
-
拷贝构造是构造函数的重载
-
拷贝构造函数的第一个参数必须是类类型对象的引用,若不使用引用,因为语法逻辑的错误会造成无穷递归
-
每次传值传参都需要调用一次拷贝构造,而调用拷贝构造函数之前先要传值传参,这样又会调用拷贝构造函数。
调用第一个拷贝构造函数给他传值传参,而传值传参又会调用拷贝构造函数,调用这个拷贝构造函数需要给它传值传参,又又会调用拷贝构造函数,一直反复循环陷入死递归
-
-
拷贝构造可以有多个参数,第一个参数是类类型的引用,后面的参数必须带缺省值
class Date
{
public:
Date(int year = 2024, int month = 8, int day = 31)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d, int i = 1)
{
//cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{
cout << "~Date()" << endl;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2 = d1;
return 0;
}
- C++规定自定义类型对象进拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 在没有优化的情况下,传值返回会生成一个临时对象,而生成这个临时对象需要调用拷贝构造函数来完成。
- 传值传参,会调用一次拷贝构造,给函数的形参,若函数没有参数就不会调用拷贝构造函数。
Date Fun1()
{
Date d;
return d;
}
int main()
{
Fun1();
return 0;
}
日期类在上述以及实现。基于VS2019 debug版本的编译环境。若是在VS2022编译环境,编译器会对调用结果进行优化。
可以观察到在调用函数Fun1传值返回后,调用了一次拷贝构造函数,生成一个临时对象,接着就将Fun1函数内的对象d析构,其次是临时对象调用析构函数。
若使用VS2022编译器,会惊人的发现,编译器只调用了一次析构函数。这是编译器在保证运行结果正确,而做出的优化。
- 我自己不定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造造成内置类型或成员变量会完成值拷贝/浅拷贝(一个一个字节的拷贝) 通过栈类举例
#include <iostream>
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new int[capacity];
_top = 0;
_capacity = 4;
}
~Stack()
{
delete[_capacity] _a;
_top = 0;
_capacity = 0;
}
void Push(int x)
{
if (_capacity == _top)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* newarr = new int[newcapacity];
_a = newarr;
_capacity = newcapacity;
}
_a[_top++] = x;
}
private:
int* _a = new int;
int _top = 0;
int _capacity = 0;
};
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
st1.Push(4);
st1.Push(5);
Stack st2 = st1;
st2 = st1;
return 0;
}
- 可以发现我们不写,编译器自动生成的拷贝构造函数,对对象st2完成了浅拷贝,使其两个对象指向同一块空间。
- st2的变化会影响到st1,对于需要申请空间管理资源的类这可是致命缺点。
浅拷贝,适用于不需要申请空间的类,类似于日期类,而栈类,他使用编译器自动生成的拷贝构造完成浅拷贝后,会造成两个栈,同时指向一块空间的情况。
这种情况在编译器自定调用析构函数时,因为后定义的先析构,st1栈先析构释放资源,st1栈后析构,但是但是同一块空间是不能析构两次的,会导致编译器崩溃,可以试试运行上面的代码。
栈这样指向的有资源的结构,不经仅将字节拷贝一份,还要将被拷贝的栈指向的资源也拷贝下来,完成深拷贝
Stack(const Stack& st)
{
_a = new int[st._capacity];
memcpy(_a, st._a, sizeof(Stack));
_top = st._top;
_capacity = st._capacity;
}
通过调用自己实现的拷贝构造函数,运行后就不会出现,编译器自动生成的拷贝构造函数的情况。
小技巧
如果一个类显示实现了析构并释放资源,那它就需要显示实现拷贝构造函数
- 传值返回会生成一个临时对象调用拷贝构造,传引用返回,返回的是返回对象的别名,不会产生拷贝,但不过函数栈帧被销毁后,栈区的返回对象的空间被销毁,最后返回一块被销毁的空间给主函数。产生野引用(举例)
Date& Fun1()
{
Date d;
return d;
}
int main()
{
Date d1 = Fun1();
cout << endl;
return 0;
}
根据前面实现的Date类,编译以上代码,编译器会报错:warning C4172: 返回局部变量或临时变量的地址: d
,不让你运行下去。
传引用返回一定要保证,函数被销毁后这个返回对象还存在。
五、运算符重载
5.1运算符重载
当运算符被用于类类型的对象是,C++语言允许通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载。
函数声明:operator 和 后面要重载的运算符组成,他也具有返回值、参数列表、函数体。
bool operator>(const Date& d1, const Date& d2)
{
}
-
该运算符是几元的,那函数就有几个参数,一元运算符有一个参数,二元运算符有两个参数。二元运算符里第一个参数对应左侧的操作数,第二个参数对应右边的操作数。
-
在调用运算符重载函数时,无论是直接调用重载函数(显示),还是使用重载后的运算符(隐式),它两的本质在编译器眼里都是一样的,都在调用重载函数。
由于在类外面实现运算符重载需要调用,对应的成员变量,而成员变量在类中是私有的。在类外定义实现其对于运算符重载,是不属于该类域,即使使用域作用限定符,语法上也是错误的。解决这中问题目前阶段最适用的就是在类中定义重载函数。
将成员变量私有变公有的方法:
- 提供对应getxxx函数。
- 友元函数。
- 重载为成员函数。
- 重载为成员函数会少一个参数,它存在一个隐含的this指针
- 同理它也可以显示调用和隐式调用
#include <iosream>
class Date
{
public:
Date(int year = 2024, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
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;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
注意事项:
- 运算符重载后的优先级和结合性必须保证与内置类型运算符保持一致。
- 无法通过链接语法中不存在的操作符进行重载
.* :: sizeof ?: .
,左边这五个运算符不能重载.*
运算符,- 运算符重载至少有一个类类型的参数,无法通过运算符重载修改内置类型对象的函数,例如:
int operator+(int x, int y)
,这里试图对修改运算符 +加号,在内置对象的功能。- 重载的运算符要有意义,比如 重载
int operator*(Date& d1, Date& d2)
,是没有意义的,日期加日期没有意义,日期加天数才有意义。- 重载运算符有前置++,和后置++,由于两个函数重载名相同,C++规定重载后置++时,需要增加形参 int,与前置++左区分。
2、
void operator@(const Date& d);
3.1、
//了解一下,老师说用的很少。
class A
{
public:
void funa()
{
cout << "void funa()" << endl;
}
};
int main()
{
void(*pf)();
pf = &A::funa();
void (A::*pf)();
pf = &A::funa;
//此时成员函数需要调用 成员函数指针时 就可以使用 .*运算符
A obj;
(obj.*pf)();
return 0;
}
//类似于以上的情况,将funa的地址赋给函数指针pf,首先它属于类域A,添上 A::来指定,
//其次,C++规定想要获取类域里的成员函数的指针、的地址,必须加上取地址符号
//最后,因为成员函数里存在隐含的this指针,最后还在 void(*pf)(); *pf前加上 A::进行限定
//这就是成员函数指针的定义
4、
6、
Date operator++(int i)//后置加加不用返回++后的值
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date& operator++()//前置加加需要返回++后的值
{
*this += 1;
return *this;//调用完函数后,*this还存在,可以使用引用返回。
}
5.2赋值运算符重载
拷贝构造:拷贝构造用于一个已经存在的对象拷贝初始化给另一个将要创建的对象。
赋值运算符重载:用于两个已经存在的对象之间直接拷贝赋值。
拷贝构造与赋值运算符之间的区别,这是易错点!
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
赋值运算符重载的连续赋值
简单,对函数加一个返回值即可
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
对返回值取别名,避免重复的调用拷贝构造。假设*this指针为d2,那返回的就是d2,出了这个函数d2也不会被销毁,它不是属于这个作用域的,所以通过取别名返回,避免重复的调用拷贝构造。提高了调用函数的效率。
这部分赋值运算符重载还可以进行优化,物为了避免两个相同的对象相互赋值
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
赋值运算符重载的特点:
-
赋值运算符重载属于运算符重载,规定必须重载为成员函数。赋值运算符重载的参数写为,const 修饰的、被引用的类类型,否则传值传参会有拷贝。
-
为了实现连续的赋值,可以使用返回值实现该功能,还需要对返回值进行引用,避免传值返回的拷贝构造,用于提高效率。
-
避免使用同一个对象相互拷贝赋值,在函数里续加上判断,的d1和d2是否相同在进行赋值。
-
若没有显示实现赋值运算符重载,编译器会自动生成一份默认赋值运算符重载,对于内置类型默认赋值运算符重载会完成浅拷贝,自定义类型成员变量会调用它对应的赋值拷贝。
小技巧:若一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要
5.3实现日期类
主要通过实现日期类的构造函数、重载函数加深对类默认成员函数、运算符重载的理解,以及应用。
class Date
{
public:
Date(int year = 2024, int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
<<、>>重载
重载流插入、流提取运算符时,需要重载为全局函数,重载为日期类的成员函数时,*this指针占用了形参的第一个位置,流插入、流提取运算符的左侧总是由cout、cin占用。
若重载为成员函数则 *this指针占用第一个位置在<<、>>
的左侧,变为:__ << cout 或 __ >> cin这不符合原始的使用习惯及降低了可读性。重载为全局函数就不会出现这种情况,但重载为成员函数又涉及了类成员变量公有和私有的问题,这里采取友元函数解决。
使得在全局定义的函数可以访问日期类中受保护或私有的成员,需注意:友元仅仅是一种声明,并不代表友元函数就是类的成员函数。
重载为全局函数需要将 ostream,istream放在第一个形参的位置,第二个位置放置类类型即可。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(iostream& in, Date& d);
public:
private:
};
在类中声明友元函数可以放在任意位置,这里我一般选择放在起始位置。
ostream& operator<<(ostream& out, const Date& d1)
{
cout << d1._year << "年" << d1._month << "月" << d1._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日" << endl;
cin >> d._year >> d._month >> d._day;
return in;
}
-
若重载流插入运算符不需要连续输出则可以选择不使用返回值
void operator<<(ostream& out, const Date& d1)
,流提取运算符同理。 -
需要连续输入、输出,则需要返回值,这个返回值必须给引用,否则会报错。
检查、获取天数
后续实现的关于日期加天数、日期减天数等均需要获取本月的日期大小来进位到下一个月份,这里将其拎出来写为一个函数,方便后续调用。
检擦函数,为保证日期的合法性进行检验,比如默认构造函数可以添加,检擦初始化日期是否合法;在重载流提取运算符添加检擦输入的日期是否合法。
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int MDarr[13] = { -1, 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 MDarr[month];
}
}
bool Check()
{
if (_month > 12 || _month < 1 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
GetMonthDay这个函数通过数组保存一年中每个月份的日期,在通过月份大小找到对应的日期,值得注意的有:
- GetMonthDay函数会被频繁调用,而每调用一次就会创建一次MonthDay数组降低效率,使用static修饰后就可以避免发生
- 若month为2月,则需要额外的判断,其是否为闰年,是返回29。
Check函数若将每一种正确情况判断完需要考虑的情景比较多,若只判断错误的日期需要考虑的情景就少了很多。
关系运算符重载
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);
类如关系运算符重载,最主要的思想就是,复用。
-
实现了,
==
这个运算符的重载,拿!=
运算符重载就可以借用==
运算符 -
接着实现了
>
大于号的重载,那就可以接着大于号,反着逻辑实现,<
小于号,又可以借助大于号、小于号、等于号实现,大于等于、小于等于。
总而言之,通过复用运算符,可以减少许多麻烦和代码。
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;
}
return false;
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
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 || *this == d;
}
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator!=(const Date& d)
{
return !(_year == d._year && _month == d._month && _day == d._day);
}
可以发现,通过复用,除了 >
大于号的代码比较多,其余运算符,在其基础上实现,若每个运算符重载,都单独写一套逻辑实现的话,未免过于冗余,麻烦。
算数、赋值运算符重载
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);//对于这种情况,不取引用,//1、是没必须,返回的只是临时值//2、返回的临时值,函数调用结束后被销毁,发生野引用
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
int operator-(const Date& d);
这块的运算符实现,关键在于 +=、-=
这两个,实现它两后,就可以复用这两个运算符实现其余的运算符,但日期类 减 日期类的运算符重载需要重新写,无法复用。
Date& Date::operator+=(const 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-=(const int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(const int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
实现 +=、-=
的重载主要思路是进位和借位。
- 第一步将参数day加上对象的_day上。
- 通过循环判断_day的日期大小是否合法,若不合法则不断循环,减去 _day对于本月的日期大小。
- _month++,若month为13则进位年year
- 思路比较简单,值得注意的细节是,当参数day小于0时,函数算法里不支持小于0的日期进行加减,需要在函数开头做一个判断,若day小于0,
*this -= -day;
- -=的函数重载思路同 +=,它是借位,需要提前借上个月的日期,在将其相加。
Date.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
//保证输入日期合法,可以写一个检查函数
friend istream& operator>>(iostream& in, Date& d);
public:
//流插入、流提取重载
Date(int year = 2024, int month = 9, int day = 3);
void Print();
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);
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int MDarr[13] = { -1, 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 MDarr[month];
}
}
bool Check()
{
if (_month > 12 || _month < 1 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);//对于这种情况,不取引用,
//1、是没必须,返回的只是临时值
//2、返回的临时值,函数调用结束后被销毁,发生野引用
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
int operator-(const Date& d);
//计算日期减日期
//思路一:1、先将本年的月日合计为天数,然后在让其相减
// 2、将年相减,将结果成上 365天,最后计算期间的闰年个数,全部相加
//思路二:1、将小的日期不断的加1,直到与大的日期相等,之间相加的个数,就是日期差
private:
int _year;
int _month;
int _day;
};
//ostream 和 istream不支持拷贝,必须使用引用
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(iostream* in, Date& d);
Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
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;
}
return false;
}
bool Date::operator<(const Date& d)
{
return !(*this >= d);
}
bool Date::operator>=(const Date& d)
{
return *this > d || *this == d;
}
bool Date::operator<=(const Date& d)
{
return *this < d || *this == d;
}
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(_year == d._year && _month == d._month && _day == d._day);
}
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& Date::operator-=(int day)
{
if (day < 0)//特别注意!!!!!!!!!!!!
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
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;
}
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
cout << "请一次输入年月日" << endl;
cin >> d._year >> d._month >> d._day;
if (!d.Check())
{
cout << "输入日期非法" << endl;
}
return in;
}
六、取地址运算符重载
6.1const成员函数
将const修饰的成员函数称为const成员函数,cons放在成员函数定义处的末尾
class Date
{
void Print() const //修饰的不是this指针,而是this本身
}
const修饰指向的内容时,才涉及权限的放大和缩小问题,
一般用来修饰不修改*this指针的成员函数,避免被const修饰的类调用成员函数导致权限放大。
const Date* d -------> Date* cosnt d :权限放大
const Date* d -------> Date const * cosnt d :权限平移
使用const修饰后的成员函数,除了普通对象可以调用,被const修饰的对象也可以调用。
而需要修改this指针指向内容的函数时,就不需要const修饰。也就是被const修饰的this指针不能修改成员变量。总的说除了需要修改成员变量的函数,其余成员函数尽可能全部加上。
6.2取地址运算符重载
int opeerator();
,前文说过想要使用运算符在自定义类型中实现,就需要运算符重载,而取地址运算符也是需要重载的,但编译器已经自生成两个取地址运算符重载函数:普通重载、被const修饰的重载。
Date* operator&()
{
//return *this;
return 00000000;
}
const Date* operator&() const
{
return *this;
}
- 这两个函数并不需要过于关注,编译器自动实现的完全够用,自己实现的话看着有比较多余了。
- 显示实现时,可以通过返回一个错误的地址,来防止别人调用自己实现的对象地址。