深入计算机语言之C++:类与对象(中)
🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:从C语言到C++语言的渐深学习
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、默认成员函数
如果一个类中什么成员都没有,我们称之为 "空类" 。但是空类中真的什么都没有吗?答案是否定的!
类有六个默认成员函数,默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。
默认成员函数很重要,也⽐较复杂,我们要从两个⽅去学习:
- 我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
二、构造函数
2.1 为什么要有构造函数
为了能够更好地讲解,我们来写一个简单的日期类,通过日期类来讲解!
#include <iostream>
using namespace std;
class Date {
public:
void SetDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
d1.SetDate(2022, 3, 8);
d1.Print();
Date d2;
d2.SetDate(2022, 3, 12);
d2.Print();
return 0;
}
对于 Date 类,我们可以通过我们写的成员函数 SetDate 给对象设置内容。但是每次创建对象都要调用这个 SetDate ,是不是感觉太麻烦了?
❓ 那有没有什么办法能在创建对象时,自动将我们要传递的内容放置进去呢?
那就要用到我们构造函数了
2.2 定义
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值(对象实例化时初始化对象),并且在对象整个生命周期内只调用一次。其特点如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
构造函数是特殊的成员函数,主要任务是初始化,而不是开空间(虽然构造函数的名字叫构造)
2.3 构造函数的使用
- 构造函数的功能就相当于我们之前书写的初始化函数(Init),但由于其自动调用的特性,大大提升了代码的容错率。
构造函数分为三种:不带参数的,带参数的,全缺省的
我们先来看看前两种的运行结果:
#include<iostream>
using namespace std;
class Date
{
public:
// 1.⽆参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
// 2. 带参数构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全缺省构造函数
//Date(int year = 1, int month = 1, int day = 1)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//不带参数构造
d1.print();
Date d2(2024,10,11);//带参数构造
d2.print();
return 0;
}
🔑 解读:不给参数时就会调用 无参构造函数,给参数则会调用 带参构造函数。
用起来很简单,但也有很多需要注意的地方:
📌 注意事项:
- 构造函数是特殊的,不是常规的成员函数,不能直接调 d1.date();
- 如果通过无参构造函数创建对象,对象后面不用跟括号,否则就成了函数声明。
2.4 默认构造函数
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
那什么是默认构造,有的同学会误以为不定义构造函数,系统自己生成的就叫默认构造,其实不然:
- 无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
- 这三个函数有且只有⼀个存在,不能同时存在。
总结:不传实参就可以调⽤的构造就叫默认构造
📌 注意事项:
全缺省和无参的不能同时使用,虽然构成函数重载,但会造成调用歧义。
🔑 解读:无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,虽然语法上允许它们们两个可以同时存在,但是如果有对象定义去调用就会报错。
并且同时,有了全缺省的构造函数,就不需要使用带参数的和不带参数的构造函数,强烈推荐实现全缺省或者半缺省,因为真的很好用:
2.5 默认构造函数的特性
通过刚才的讲解我们知道如果你没有自己定义构造函数,C++ 编译器会自动生成一个无参的默认构造函数。当然,如果你自己定义了,编译器就不会帮你生成了。
那我们就让编译器自己生成一个试试:
#include<iostream>
using namespace std;
class Date
{
public:
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1; // 这里调用的是默认生成的无参的构造函数
d1.print();
return 0;
}
在我们不是先构造函数的情况下,编译器生成的默认构造函数。
"似乎这看起来没有什么鸟用啊,这不就是一堆随机值嘛……"
d1 对象调用了编译器生成的默认函数,但 d1 对象 year / month / day 依旧是随机值
🔑 解答:C++ 把类型分成内置类型(基本类型)和自定义类型。
- 内置类型就是语法已经定义好的类型:如 int / char...
- 自定义类型就是我们使用 class / struct / union / stack 自己定义的类型。
C++ 规定:我们不写编译器默认生成构造函数对于内置类型的成员变量不做初始化处理。
class A
{
public:
A()
{
cout << "hello" << endl;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
A b;
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Print();
return 0;
}
上图中编译器对于class 类型的 b 进行了初始化(自定义类型(class) b 的初始化就是调用 b 的构造函数),而对于内置类型就没有进行初始化
而如果自定义类型 b 没有默认构造函数就会报错
"你要写就写好了,要么就别写,不写我默认生成的能保底"
三、析构函数
通过前面构造函数的学习,我们知道了一个对象是怎么来的
那一个对象又是怎么没的呢?既然构造函数的本质是初始化,那清理的工作交给谁来干呢?那就要交给析构函数了。
3.1 定义
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象是存在栈帧的,函数结束栈帧销毁。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作,类似我们之前Stack实现的Destroy功能。其特点与构造函数类似,如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(但常常不能满足我们的需要)。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
3.2 析构函数的使用
我们知道了,如果没写析构函数编译器会自动生成一个。那默认生成的析构函数会做什么事情呢?它会帮我们 destroy 嘛?
哪有这种好事,不能什么都帮你做啊!
我们刚才在构造函数中讲了:
📌 如果不自己写构造函数,让编译器自动生成,那么这个自动生成的 默认构造函数:
- 对于 "内置类型" 的成员变量:不会做初始化处理。
- 对于 "自定义类型" 的成员变量:会调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错!
而我们的析构函数也是这样的,也与之类似!
📌 如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:
- 对于 "内置类型" 的成员变量:不作处理 (不会帮你清理的.)
- 对于 "自定义类型" 的成员变量:会调用它对应的析构函数 (虽然大多数时候并没有什么用,但已经仁至义尽了) 。
" 编译器:哈哈哈,给你默认生成个用用就不错了,你都懒得写了,不要挑三拣四滴!"
难道就不能帮我把这些事都干了吗?帮我都销毁掉不就好了?不不不,举个最简单的例子,迭代器,析构的时候是不释放的,因为不需要他来管,所以默认不对内置类型处理是正常的,万一误杀了怎么办,对吧。
有人可能又要说了,这么一来默认生成的析构函数不就没有用了吗?
有用!他对内置类型的成员类型不作处理,会在一些情况下非常的有用!比如说: 两个栈实现一个队列,用 C++ 可以非常的爽。
typedef int STDataType;
class Stack
{
public:
Stack(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
//自动调用构造函数和析构函数
private:
Stack pushst;
Stack popst;
};
注意:⼀个局部域的多个对象,C++规定后定义的先析构,参考上述代码即:先析构 _top ,后析构 _capacity,与栈的规则类似(栈先进后出)。
需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员(下列代码中的Stack)无论什么情况(哪怕对MyQueue进行析构)都会自动调用析构函数(~Stack),防止造成严重的内存泄漏问题。至于其他资源,比如指针(STDataType* _a)等需要我们自己进行释放(在~Stack中写的free)。
如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
四、拷贝构造
4.1 定义
我们在创建对象的时候,能不能创建一个与某一个对象一模一样的新对象呢?当然可以,这时我们就可以用拷贝构造函数。
📚 拷贝构造函数:拷贝构造函数是构造函数的一种重载形式,它可以用来创建一个与已存在的对象一模一样的新对象。对于拷贝构造,它只有单个形参,且该形参必须是对本类类型对象的引用,因为要引用,所以一般要加const修饰
拷贝构造也是一个特殊的成员函数,所以他符合构造函数的一些特性::
- 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 拷贝构造函数的参数只有一个,并且 必须要使用引用传参。
- 使用传值方式会引发无穷递归调用!
"拷贝构造函数的引用是必不可少的!"
4.2 拷贝构造的使用
💬拷贝构造的代码示例如下:
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
/* Date d2(d1); */
Date(const Date& d) // 这里要用引用,否则就会无穷递归下去
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 3, 9);
Date d2(d1); // 拷贝复制
//Date d2 = d1; 和 Date d2(d1); 的效果是一样的
// 看看拷贝成功没
d1.Print();
d2.Print();
return 0;
}
其中 Date d2(d1); 和 Date d2 = d1; 达成的效果是完全一样的,可以选择自己喜欢的写法
4.3 为什么要使用引用传参
❓ 拷贝构造的时候直接使用 Date d 来传值传参可以吗?
编译器会直接报错
但这是为什么呢?
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
调用拷贝构造,需要传值传参,传值传参需要调用拷贝构造。
……
一直在传参这里出不去了,一直在进行自己调用自己,所以这里是一个无穷递归。
如上图所示,执行date d2(d1); d1传参给拷贝构造的形参d,即需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参,那么现在的对象只有d1了,所以会出现 date d(d1),而拷贝的过程中又会调用自身的拷贝构造函数,传值方式会继续传进一个A的对象作为实参,会无休止的递归下去。
而加上引用,则形参d是d1的别名,就不需要继续进行拷贝构造。
❓ 拷贝构造函数加 const:如果函数内不需要改变,建议把 const 也给它加上!
万一你不小心写反了怎么办?
/* Date d2(d1); */
Date(Date& d) {
d._year = _year;
d._month = _month;
d._day = _day;
}
这样会产生一个很诡异的问题,这一个可以被编译出来的 BUG ,结果会变为随机值。所以,这里加一个 const 就安全多了,这些错误就会被检查出来了。
4.4 默认生成的拷贝构造
📚 默认生成拷贝构造:
- 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去),也叫做浅拷贝。
- 自定义类型成员,会再调用它的拷贝构造。
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// Date(Date& d) {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2002, 4, 8);
// 拷贝复制
Date d2(d1);
// 没有写拷贝构造,但是也拷贝成功了
d1.Print();
d2.Print();
return 0;
}
🔑 他这和之前几个不同了,这个他还真给我解决了。
所以为什么要写拷贝构造?写他有什么意义?没有什么意义。
默认生成的一般情况下就够用了!当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的,来查看如下代码:
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(s1);//默认拷贝构造
return 0;
}
这段程序会发生崩溃,让我们来调试以下看看什么情况
我们会发现s1和s2的成员变量_array都指向同一块空间,也就说编译器调用系统生成的默认构造时,把s1._array指向的地址也拷贝给了s2._array,这看起来没什么,等到程序结束完成时,调用析构函数会出现大问题!
程序即将结束时,调用析构函数,会先析构s2,第一次析构完成后,s2._array指向的空间会被释放,其他置为0,也就是等价于s1._array指向的空间也被释放(同一块空间),此时再执行s1的析构函数时,导致他们指向的空间被析构两次,导致程序崩溃然而问题不止这些……
其实这里的字节序拷贝是浅拷贝,下面几章我会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。
🔺 总结:对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于自定义类型中需要资源申请时,都需要手动写拷贝构造,默认生成的拷贝构造不能用。
五、赋值运算符重载
5.1 运算符重载
5.1.1 运算符重载的定义
C++为了增强代码的可读性引入了运算符重载,那么什么是运算符重载呢?
运算符重载是由运算符 operator 定义具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。简单来说就是:能让自定义类型和内置类型一样使用运算符。5.1.2 运算符重载的注意事项
① 不能通过连接其他符号来创建新的操作符, 你只能对已有的运算符进行重载,你也不能对内置类型进行重载。
operator@ ❌
② 重载操作符必须有一个类类型的操作数。
③ 用于内置类型的操作符,其含义不能改变。比如内置的 整型 +,你不能改变其含义。
④ 作为类成员的重载函数时,其形参看起来比操作数数目少 1,成员函数的操作符有一个默认的形参 this,限定为第一个形参。
5.1.2 重载函数的使用
💬 代码演示:运算符重载 ==,下面实现了简单判断日期是否相当的运算符重载:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
bool operator == (const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 1, 1);
Date d2(2024, 1, 1);
if (d1 == d2)//也可以显示调用operator==(d1,d2);
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}
return 0;
}
这里会发现运算符重载成全局的,不得不将成员变量是共有的,得把 private 撤掉:
但这种情况下会导致我们想要私有化的内容公开出来
5.1.3 如何保证封装性
❓ 那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决!
当然,我们现在还没有学习到 "友元",可以通过将 operator 重载成成员函数来解决
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 1, 1);
Date d2(2024, 1, 1);
if (d1 == d2)//也可以显示调用operator==(d1,d2);
{
cout << "日期相等" << endl;
}
else
{
cout << "日期不相等" << endl;
}
return 0;
}
5.1.4 不支持重载的运算符
注意,有五个运算符不支持重载!慎之慎之!
. (点运算符)
:: (域运算符)
.* (点星运算符,)
?: (条件运算符)
sizeof
值得一提的是:虽然点运算符 . 不能重载,但是箭头运算符 -> 是支持重载的,解引用 * 是可以重载的,不能重载的是点星运算符 .*
"网络上不知名的冲浪大神发明的,个人感觉挺不错。"
5.2 赋值运算符重载
5.2.1 定义
赋值运算符重载是将运算符 =
进行运算符重载。但是它相较于其他运算符重载有着自己独特的特点。
需要注意的是,赋值运算符重载也是默认成员函数。
📚作为默认成员函数的 operator 有着如下特点:
- 参数类型:const T& ,传递引用可以提高传参效率。
- 返回值类型:T& ,返回引用可以提高返回的效率,支持连续赋值。
- 检测是否自己给自己赋值。
- 返回* this :要复合连续赋值的值。
5.2.2 赋值运算符重载的使用
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)//赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 3, 10);
Date d2(2022, 7, 1);
d1 = d2;
return 0;
}
5.2.3 使用引用可以有效减少拷贝
返回值为什么要用引用?因为出了作用域 *this 还在,所以我们可以使用引用来减少拷贝!
我们先把引用返回去掉:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
cout << "调用了一次拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
Date operator=(const Date& d)//赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 3, 10);
Date d2(2022, 7, 1);
Date d3(d2); // 拷贝构造
d1 = d2;
return 0;
}
🚩 运行结果如下:
我们发现,调用了两次拷贝构造函数。
我们来进行调试一下:
① 第一句 "调用了一次拷贝构造" 是因为 Date d3(d2) ,我们自己调用的。
② 第二句则出自 d1 = d2
因为传值返回不会直接返回对象,而是会生成一个拷贝的对象。
我们这里出了作用域,对象还在,就可以使用引用返回:
Date& operator=(const Date& d)//赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
成功减少了拷贝!
我们也就可以得出结论:
- 赋值传参如果不加引用,会先去调用拷贝构造,再进入operator进行赋值,operator本身不像拷贝构造一样无穷递归
- 但operator传参加入引用,就不会先去调用拷贝构造
5.3 实践:日期类的实现
我们学习了上面的知识,来尝试实现一个日期类的代码吧
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
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 operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
我们获取当月日期的函数经常需要调用,所以我们就将其写在定义里面。
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
// 365天 5h +
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year
% 400 == 0))
{
return 29;
}
else
{
return monthDayArray[month];
}
}
#include"Date.h"
bool Date::CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}
// 全缺省的构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
if (!CheckDate())
{
cout << "⽇期⾮法" << endl;
}
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
// 拷贝构造函数
// d2(d1)
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
Date::~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 日期+=天数
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)
{
_month = 1;
_year++;
}
}
return *this;
}
// 日期+天数
Date Date::operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
// 日期-天数
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)
{
_day += GetMonthDay(_year, _month);
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
}
return *this;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// 后置--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if(_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if(_month == d._month)
{
return _day > d._day;
}
}
return false;
}
// ==运算符重载
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);
}
// <=运算符重载
bool Date::operator <= (const Date& d)
{
return !(*this > d);
return true;
}
// !=运算符重载
bool Date::operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
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 (max != min)
{
min++;
n++;
}
return n * flag;
}
六、取地址重载
6.1 const 修饰成员函数
首先我们得知道一个规则就是,const修饰的常变量不能赋值给普通变量,因为这样造成const权限的放大,但是普通变量可以赋值给const修饰的常变量。所以让我们来看看这段代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
const Date d1(2022, 1, 13);
d1.Print();//error
return 0;
}
这段代码会出错,因为 d2 调用 Print 函数是将 const Date* 作为参数类型传过去,而函数接受的类型是 Date* ,这样就会造成权限的放大。为了解决这个问题,就需要使用const修饰原函数:
void Print() const
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
6.2 取地址运算符
取地址运算符和 const取地址运算符重载也是默认成员函数:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
💬这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到你的地址:
#include <iostream>
using namespace std;
class Date {
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
// return this; 我不想让你取我的地址
return nullptr;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 2, 2);
cout << &d1 << endl;
return 0;
}
或者这样:
Date* operator&()
{
// return this; 我想误导你
return 0xFE087460;
}
返回一个看似正常的地址,让你的同学调用你的库的时候摸不着头脑,但需要慎用!