operater==
注: == 的优先级比 << 的优先级低。
上图的operator==函数就是比较两个日期是否相等的函数。如果该函数不再类中定义的话,那么就需要将成员变量改成公有public。如果这样子做的话,那封装的意义就不存在了。
如果我们既想要运算符重载,又想成员变量为私有private。那如何解决呢?这时候我们可以借助友元(类和对象下的内容)或者在类中定义一个辅助的函数(Java经常使用这种方式),还可以将运算符重载定义在类中了。在这里,我们采用将运算符重载定义在类中这种方式。那operator==如何定义呢?见下图代码:
//... bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } //...
看到上面的代码,可能就会有细心的小伙伴发现,怎么operator==的参数只有一个,是不是写错了呀。其实并没有写错,因为每个成员函数会有一个隐藏参数this,该参数占据成员函数的第一个参数的位置。
注:编译非常的智能:如果我们运算符重载在类中定义了,编译器就不会去全局中找;如果运算符重载没有在类中定义,那么编译器就会去全局中找。
operator>
有时候,我们需要比较两个日期的大小,那我们来看一下operator>的代码。
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; }
operator>=
比较一个日期 d1 是否大于或等于另外一个日期 d2,那么就可以赋用上面的operator==
和operator>
。
operator>= 比较一个日期 d1 是否大于或等于另外一个日期 d2,那么就可以赋用上面的operator==和operator>。
注:*this
就是日期 d1。
operator<=
operator<=
重载运算符也可以赋用operator>
运算符,因为operator<=
是operator>
的反面,那我们一起来看一下代码。
//.. bool operator<=(const Date& d) { return !(*this > d); } //...
operator<
因为operator<
是operator>=
的反面,所以可以赋用operator>=
。
//... bool operator<(const Date& d) { return !(*this >= d); } //...
operator!=
因为operator!=
是operator==
的反面,所以可以赋用operator==
。
//... bool operator!=(const Date & d) { return !(*this == d); } //...
operator+= 和 operator+
如果我们想算一个某一天的 N 天后是哪一天,这时候就需要借助operator+=
或者operator+
。注意,operator+=
和operator+
的返回值为Date
。日期的加法是比较复杂的,因为每个月有多少天是没有规律的。
因为每个月的天数是没有规律的,所以我们就写一个函数来得到每个月的天数,然后再进行日期的加法。
获取每个月的天数
//... // 获取每个月的天数 int GetMonthDay(int year, int month) { // static修饰数组避免频繁创建 static int monthDayArray[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; } else { return monthDayArray[month]; } } //...
operator+=
//... Date& operator+=(int day) { // 处理 day < 0的情况 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; } //...
注:+=
运算符会修改变量的值,而且出了函数的作用域,+=
后的对象还存在,所以operator+=
的函数返回值为Date&
。注:如果是值返回也会存在拷贝。
operator+
因为+运算符不会影响变量的值,所以我们借助拷贝构造来创建一个对象ret,然后赋用operator+=重载运算符让ret += day,最后将ret返回。注意:出了函数作用域,ret就不存在了,所以operator+的返回值为Date,不能是Date&。
//... Date operator+(int day) { Date ret(*this); ret += day; return ret; } //...
operator-= 和 operator-
operator-=
//... Date& operator-=(int day) { // 处理 day < 0的情况 if (day < 0) { return *this += -day; } _day -= day; while (_day <= 0) { --_month; if (_month == 0) { --_year; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } //...
operator-
Date operator-(int day) { Date ret(*this); ret -= day; return ret; }
前置++ 和后置++ 重载
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确的函数重载。C++规定:后置++重载时多增加一个int类型的参数来区分前置++和后置++,但调用函数时该参数不用传递,编译器自动传递。
前置++
前置++:返回+1之后的结果。注意:
this
指向的对象函数结束后不会销毁,故以引用方式返回提高效率。
//... // 前置++ Date& operator++() { *this += 1; return *this; } //...
后置++
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将
*this
保存一份,然后给*this
+1。而 tmp 是临时对象,因此只能以值的方式返回,不能返回引用。
注:对于内置类型,使用前置++或后置++的区别不大;但对于自定义类型需要++时,建议使用前置++,因为使用后置++会多两次拷贝构造。
前置-- 和后置-- 重载
前置–
//... // 前置-- Date operator--() { *this -= 1; return *this; } //...
后置–
//... // 后置-- Date operator--(int) { Date tmp(*this); *this -= 1; return tmp; } //...
日期 - 日期
//... // 日期 - 日期 int operator-(const Date& d) { Date max = *this; Date min = d; int flag = 1; // 注意:不要写出 d > *this // 这样子写涉及引用权限的放大和缩小 // 后面的内容会讲解这个知识点 if (*this < d) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { ++min; ++n; } return n * flag; } //...
operator<< 和 operator>>
现在日期类的功能已经实现了差不多了,现在就还差cin >>
和cout <<
的功能了。我们现在把这两个功能实现一下,不过这个知识点相对来说比较难。不过也不要太担心,有我在呢!
学习这个之前,我们需要知道一些前置知识。cin是头文件istream里的对象,而cout是头文件ostream里的对象。>>是流提取运算符,而<<是流插入运算符。istream和ostream也是类。
知道了这些,我想问大家一个问题:为什么cin和cout能够自动识别类型呢?其实这背后的原理就是函数重载和运算符重载。如下图所示:
operator<<
cin
和cout
默认就支持内置类型的函数重载和运算符重载,而不支持自定义类型的函数重载和运算符重载。这时候,就要发挥我们智慧的大脑了,自己动手丰衣足食。
//... // d1 << cout void operator<<(ostream& out) { out << _year << "年" << _month << "月" << _day << "日" << endl; } //...
上面operator<<虽然可以实现输出日期的功能,但是和我们的使用习惯相反且不具有可读性。所以一般情况下,流提取重载和流插入重载都不会定义在类中。那怎么解决这个问题呢?我们可以将operator<<定义成全局函数。这样又会带来应该问题,就是封装的问题。如果我们将operator<<定义成全局函数,就需要将成员变量的属性改成公有public。那我们先试试先吧,实现出来再看看还有没有更好的办法。
//... void operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; } //...
这个operator<<的写法还有可以优化的地方。因为这个写法不能输出多个日期,那么将函数的返回改成ostream&就可以了。
//... ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } //...
虽然operator<<
这样子写解决了可读性和使用习惯的问题,但是又带来了更大的问题——封装的问题。那怎么解决呢?接下来,我们的友元就要上场表演了。
注:如果不借助友元,operator>>
也会出现上述的问题。
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。 通俗来讲,就是声明这个函数是友好的,不会直接修改成员变量的值。
注:友元函数将会在类和对象下详细讲解。友元声明可以在类中的任意位置。
//... class Date { //友元声明 friend ostream& operator<<(ostream& out, const Date& d); //... } // cout << d1 operator<<(cout, d1) inline ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; }
有了友元函数,就算类的成员变量的属性为私有private,也可以在类外访问类的成员变量了。
注:operator<<运算符重载很有可能会经常被调用,那么我们可以将它改成内联函数。友元声明时不需要加上inline,定义的时候需要加上inline。
有了operator<<运算符重载,那么成员函数Print也就可以退休了。关于operator<<运算符重载的知识点就这些了,我们现在来学习一下operator>>运算符重载。
operator>>
//... Date { //友元声明 friend istream& operator>>(istream& in, Date& d); //... } // cin >> d1 operator>>(cin, d1) inline istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; }
注:有关operator>>
的知识点和operator<<
的知识点相似。
👉赋值运算符重载👈
operator=
运算符为赋值运算符重载,那我们来看一下赋值运算符重载的格式。
赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
注:赋值运算符重载既是默认成员函数,又是运算符重载。
//... Date& operator=(const Date& d) { if(this != &d) // 避免 d1 = d1 的情况 { _year = d._year; _month = d._month; _day = d._day; } return *this; // 返回左操作数 } //...
注:赋值运算符重载的参数也可以是类对象,但传参的时候需要调用拷贝构造函数。返回值为类对象的引用,既可以提高返回的效率(值返回时会调用拷贝构造函数),又可以实现连续赋值。赋值运算符重载的引用返回一般不用 const 修饰,因为有可能该对象还要修改。
赋值运算符重载是一个默认成员函数,如果自己不写编译器会自动生成。那我们现在就不写赋值运算符重载,看看会有什么情况发生。
将赋值运算符重载屏蔽后,再将程序运行起来,我们可以发现还是可以完成赋值的。那为什么呢?其实是,如果我们不写赋值运算符重载,对于内置类型会完成值拷贝,对于自定义类型会调用该自定义类型的运算符重载。 所以如果我们不写日期类的赋值运算符重载,也能完成拷贝。
那如果栈Stack和队列MyQueue不写赋值运算符重载,又会发生什么呢?
上面的栈Stack还没有写赋值运算符重载,然后运行起来就崩溃了。因为栈Stack的成员都是内置类型,那么编译器生成的赋值运算符重载会完成值拷贝。那么赋值过后,st1 和 st2就都执行了同一块空间。到了析构的时候,就会对同一块空间析构多次,然后程序就崩溃了。而且还会带来一个很严重的问题就是内存泄漏。
对于栈Stack来说,编译器默认生成的赋值运算符重载不能用,那么就需要我们自己写了。因为 st1 和 st2 的空间大小情况不清楚,所以我们先把 st1 原来的空间先释放掉,再申请一块和 st2 一样大的空间,然后再把数据拷贝过去。
//... Stack& operator=(const Stack& st) { if(this != &st) // 避免 st1 = st1 的情况 { free(_a); _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; _capacity = st._capacity; } return *this; } //...
日期类不需要自己写赋值运算符重载,栈Stack需要自己写赋值运算符重载,那队列MyQueue需不需要自己写呢?我们一起来看一下。
可以看到,队列MyQueue也不需要写赋值运算符重载。那我们来总结一下什么类需要写赋值运算符重载。像拷贝构造函数一样,如果该类需要写析构函数,那么就需要写赋值运算符重载;如果该类不需要写析构函数,那么就不需要写赋值运算符重载。
赋值运算符只能重载成类的成员函数不能重载成全局函数原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
👉const 成员函数👈
将const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰成员函数时,只会修饰 this 指针,并不会修饰成员函数的其它参数。
知道了const修饰成员函数,我们现在来看一个例子:
可以看到,当用 const 修饰一个日期时,该日期就不能再修改了。当 d2 调用Print函数时, 编译器会将 d2 的地址转换成 this 指针,该 this 指针的类型为Date* const,相当于 this 不能被修改,但是 this 指针指向的空间里的内容可以修改。又因为 d2 用了const修饰,&d2 的类型是const Date*,所以这就涉及指针权限的权限放大和缩小。
那如何解决呢?就是在Print函数后面加上个const关键字修饰。
类中的成员函数的参数很多都需要用const修饰,也不会修改 this 指针指向的内容,所以很多成员函数都需要用const来修饰。那么什么成员函数要用const修饰呢?不会修改 this 指针指向的内容的成员函数就需要用const来修饰。大家可以给以上写的成员函数加上const。
给大家留几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
👉取地址及const取地址操作符重载👈
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。不过,我们也把这两个函数实现一下。
//... Date* operator&() { return this; } const Date* operator&() const { return this; } //...
如果这两个函数不写也没有什么问题,编译器生成也够用。如果你不想让别人拿到类对象的地址就可以像下面这样写。
Date* operator&() { return nullptr; } const Date* operator&() const { return nullptr; } //...
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
👉总结👈
本篇博客主要讲解了构造函数、析构函数、拷贝构造函数、赋值运算符重载、运算符重载、const修饰成员函数以及取地址及const取地址操作符重载,这些内容是学习后面内容的基础,希望大家能够掌握。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️