类和对象(中)
片头
大家好!在上一篇中,我们初步了解了类和对象,今天我们继续深入学习类和对象,准备好了吗?咱们开始咯!
一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。
class Date {};
用户本身没有去显示实现,编译器会自己生成的成员函数就称为默认成员函数。
二、构造函数
2.1 概念
对于以下的Date类:
#include<iostream>
using namespace std;
class Date {
public:
//成员函数公有化
void Init(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() {
Date d1; //创建d1对象
d1.Init(2024, 8, 26);//将d1初始化
d1.Print(); //打印d1对象里面的数据
return 0;
}
运行结果如下:
对于Date类,可以通过Init公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能不能在创建对象的同时,直接将对象初始化呢?
构造函数就可以实现这个需求,它是一个特殊的成员函数,名字与类名相同,创建对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次,其他时候不会调用。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。(类似Init函数的功能)
其特征如下:
(1)函数名与类名相同
(2)无返回值(无需写void)
(3)对象实例化时编译器自动调用对应的构造函数
(4)构造函数可以重载(多个构造函数,有多种初始化方式)
#include<iostream>
using namespace std;
class Date {
public:
//1.无参构造函数
Date() {
_year = 2024;
_month = 8;
_day = 26;
}
//2.有参构造函数
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;
};
当我们实例化对象不传参,调用第一个无参构造函数;当我实例化对象传参时,就会调用第二个有参构造函数。
有的小伙伴可能有疑问:为啥调用无参构造函数后面不用加括号呢?因为如果加上括号,那么无参函数就和函数声明很难区分了。
所以,使用无参构造时初始化对象时,不用带括号!当我们调用无参构造函数时,直接创建对象即可,编译器会自动调用无参构造函数。
(5)如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。
以下是我们没有写有参构造函数的情况,编译器自动提供无参构造函数。
而当我们自己写了有参构造函数,再去调用默认的无参构造函数,系统会报错
因为已经有了显式定义的构造函数,编译器就不再生成无参构造函数。
这是为什么呢?编译器会自动生成默认的无参函数,好像也没有什么作用呀?
原来,C++把类型分为了内置类型(基本类型)和自定义类型。内置类型就是语言自己提供的,例如:int、char等类型,而自定义类型就是我们使用class/struct/union等自己定义的类型。
例如,我们在日期类中添加一个自定义类型的成员函数_time
#include<iostream>
using namespace std;
class Time {
public:
Time() {
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private:
int _year;
int _month;
int _day;
Time _time;
};
接着我们创建Date类的对象,结果如下:
可以看到,Date类中编译器生成的默认构造函数对自定义类型的变量_time起了作用,去调用了Time类的构造函数。
编译器自动生成的默认构造函数,只对自定义类型起作用,而不处理内置类型。 我们的日期类中原本的三个成员变量都是内置类型,所以才会出现随机值。
实际上,这是C++的一个缺陷,所以在C++11中针对内置类型成员不初始化的缺陷打了一个补丁,即内置类型的成员变量在声明时可以给默认值。
(6) 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有1个。
比如:
#include<iostream>
using namespace std;
class Date {
public:
//无参构造函数
Date() {
_year = 2024;
_month = 8;
_day = 26;
}
//全缺省构造函数
Date(int year = 2026, 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();
return 0;
}
我们尝试运行一下:
一个无参构造函数,一个全缺省构造函数,那么此时实例化d1时该调用哪个函数呢?
最好就直接把无参的构造函数删了就行~,因为全缺省的构造函数也能完成无参的功能呀!
三、析构函数
3.1 概念
通过前面构造函数的学习,我们知道了一个对象是怎么来的,那一个对象又是怎么没得呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。(类似于Destroy函数功能,这个函数会销毁开辟出的空间,避免内存泄漏)
3.2 特性
析构函数是特殊的成员函数,其特征如下:
(1)析构函数名是在类名前加上字符~
(2)无参数无返回值类型
(3)一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的析构函数。注意:析构函数不能重载!
(4)对象生命周期结束时,C++编译系统会自动调用析构函数
我们以栈为例子,写一个析构函数
#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int DataType;
class Stack {
public:
Stack(size_t capacity = 4) {
_array =(DataType*) malloc(sizeof(DataType) * capacity);
if (nullptr == _array) {
perror("malloc fail!\n");
exit(1);
}
_capacity = capacity;
_top = 0;
}
void Push(DataType data) {
//CheckCapacity();
_array[_top] = data;
_top++;
}
//其他方法....
//析构函数
~Stack() {
if (_array) {
free(_array);
_array = NULL;
_capacity = 0;
_top = 0;
}
}
private:
DataType* _array;
int _capacity;
int _top;
};
有了析构函数,我们对栈进行各种增删查改后再也不用手动释放空间了,编译器会自动完成。
(5)编译器自动生成的默认析构函数只对自定义类型的成员有效
因为内置类型的成员变量出了作用域就销毁了,不需要进行回收空间等操作,而自定义类型的成员则需要额外的清理。
我们还是添加一个Time类型做例子:
我们看到,程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为,main方法中创建了Date对象d1,而d1中包含4个成员变量,其中_year,_month,_day 这3个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d1销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。
但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显示提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。
main函数中并没有直接调用Time类的析构函数,而是显示调用编译器为Date类生成的默认析构函数。注意:创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数。
总结:
①虽然我们创建的是Date类的对象,但是还是调用了Time类的析构函数
②因为在Date类中有类型为Time的成员变量,在销毁d1时要先销毁Time类的成员变量_t
③编译器会在Date类中自动生成默认的析构函数并调用,这个函数自身再去调用Time类的析构函数
④对于3个int类型的成员变量,因为是内置类型,所以析构函数不做处理
所以,如果类中没有申请资源时(只有内置类型的成员变量),析构函数可以不写,直接使用编译器生成的默认析构函数。比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
四、拷贝构造函数
4.1 概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一模一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
比如:
我们在创建一个内置类型的变量时,可以用另一个变量去初始化
int b = 10;
int a = b;
对于变量,我们也想这样很方便快捷的去创建一个和已存在对象一模一样的新对象,于是就有了拷贝构造函数。
在用已存在的类类型对象去创建新对象时,编译器会自动调用拷贝构造函数。
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
(1)拷贝构造函数是构造函数的一个重载形式。
(2)拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
正确写法:
用法:
Q1:为什么形参前要加const呢?
看看下面这种情况
如果实参本身就是被const修饰的,而形参又没加const,就会造成权限放大。
Q2:为什么形参一定要用引用呢?传值传参不行吗?
如果对于自定义类型的对象,我们在拷贝构造函数中使用传值传参的话,会引发无限递归。
我们知道,传值传参时,形参是实参的拷贝。
编译器对于内置类型直接进行浅拷贝,即按字节拷贝;对于更复杂的自定义类型,编译器不敢擅自拷贝,此时就需要调用类的拷贝构造函数。
对于自定义类型为什么不能浅拷贝呢?例如栈这种类型,其内部存放的是指向空间的指针,我们对栈进行拷贝,想要的效果是两个栈各自拥有自己的空间,空间内的元素相同;如果我们对其进行浅拷贝的话,就只是对地址进行了拷贝,两个栈就指向了同一块空间,最后两个栈调用析构函数时就对这块空间清理了2遍,程序会崩溃。
但是如果你的拷贝构造函数中使用了传值传参,那么就会造成死递归,即拷贝构造需要传值传参,但是每次传值传参又要调用拷贝构造。
(3)若用户未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数只会进行浅拷贝(值拷贝),即按字节拷贝。
我们把日期类中显示定义的拷贝构造函数删除,看看编译器生成的默认拷贝构造函数是否有效
由于日期类中只有内置类型的成员变量,所以使用编译器生成的默认拷贝构造也可以。
因此,如果类中没有涉及资源申请时,可以选择用编译器生成的默认拷贝构造;一旦涉及到资源申请时,就一定要自己写拷贝构造函数。
五、赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 这五个运算符不能重载(常在笔试题中出现)
在之前的学习中,我们可以使用>(大于号)、<(小于号)等运算符来比较两个内置类型的变量;但是对于像日期类的对象而言,当我们想比较两个日期时,无法使用这些运算符来进行比较
因此,为了解决这个问题,C++引入了运算符重载
运算符重载是具有特殊函数名的函数,这里要提到关键字operator
函数名:operator + 待重载的运算符
例如我要对相等运算符==进行重载,相等返回true,不相等返回false,那么函数名就是
bool operator==(参数列表)
5.2 小贴士:
(1)我们只能在operator后加上已经存在的运算符进行重载,不能凭空创造一个新的操作符
例:不能写一个operator#并定义它,不存在名为#的运算符
(2)关于运算符重载的参数和参数类型:
首先,我们在类中定义运算符重载函数
还是以相等运算符==为例,我们知道它有2个操作数,那么是不是意味着相等运算符的重载函数也要2个参数呢?
可以看到,报错的原因是:参数过多
你是否忘记了一点:成员函数的第一个参数为隐式的this指针?
所以实际上的确有2个参数,但是一个是隐式的this指针,一个是显式的类类型对象
正确的函数如下:
如果我们在类外定义运算符重载函数呢?
因为,在类外定义的函数没有隐式的this指针,所以我们需要写2个参数
咦?我们无法在类外访问私有的成员变量啊!
针对这种情况,我们怎么做呢?
方法一:将Date类的成员变量全部公有化
方法二:提供Date类成员变量的get和set方法
方法三:友元
方法四:重载为成员函数(一般使用这种)
方法一: 将Date类的成员变量全部公有化
#include<iostream>
using namespace std;
class Date {
public :
Date(int year = 2026, int month = 6, int day = 1) {
_year = year;
_month = month;
_day = day;
}
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, 9, 2);
Date d2(2024, 9, 3);
cout << (d1 == d2) << endl;
return 0;
}
方法二:提供Date类成员变量的get和set方法
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 2024, int month = 9, int day = 3) {
_year = year;
_month = month;
_day = day;
}
//Year的Set方法
void SetYear(int year) {
this->_year = year;
}
//Year的Get方法
int GetYear() {
return _year;
}
//month的Set方法
void SetMonth(int month) {
this->_month = month;
}
//month的Get方法
int GetMonth() {
return _month;
}
//Day的Set方法
void SetDay(int day) {
this->_day = day;
}
//Day的Get方法
int GetDay() {
return _day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(Date& d1, Date& d2) {
return d1.GetYear() == d2.GetYear()
&& d1.GetMonth() == d2.GetMonth()
&& d1.GetDay() == d2.GetDay();
}
int main() {
Date d3(2024, 6, 1);
Date d4(2024, 6, 2);
operator==(d3, d4);
d3 == d4;
return 0;
}
方法三:友元
#include<iostream>
using namespace std;
class Date {
//友元代码
friend bool operator==(const Date& d1, const Date& d2);
public:
Date(int year = 2024, int month = 9, int day = 3) {
_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 d3(2024, 6, 1);
Date d4(2024, 6, 2);
operator==(d3, d4);
d3 == d4;
return 0;
}
方法四:重载为成员函数(一般使用这种)
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 2024, int month = 9, int day = 3) {
_year = year;
_month = month;
_day = day;
}
//成员函数的第一个参数为隐藏的this
//bool operator==(Date* this,const Date& d)
bool operator==(const Date& d) {
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d3(2024, 6, 1);
Date d4(2024, 6, 2);
d3.operator==(d4);
d3 == d4;
return 0;
}
这个部分在刚开始已经讲过,这里不再重复赘述。
至于相等运算符的重载函数用法如下:
编译器会将 d3 == d4 转换成 d3.operator==(d4)
因为流插入运算符的优先级较高,我们需要用括号把待比较的对象和运算符括起来
(3)重载后的运算符,其特性应和原本的运算符一样
例如加法运算符“+”,我们想要实现在一个日期的基础上加上一个天数,获得一个新的日期,就需要对其进行运算符重载
我们来统计一下“+”的特性有哪些
- 可以"+"一个负数
- 可以进行连续的“+”,例如 a+b+c
- 一个变量“+”一个数,变量本身不会改变
所以在重载的加法运算符函数中,我们需要对形参的正负进行判断,并且要返回日期类的对象保持连续运算的特性,还要用拷贝构造函数创建一个临时的对象来进行运算,避免修改对象本身。
如果天数为负数,我们就可以调用减法运算符的重载函数。
因为temp出了作用域就销毁了,所以我们无法传引用返回,只能传值返回temp的拷贝
(4)重载运算符函数中必须至少有一个类类型的参数
重载运算符本来就是用来运算自定义类型的,如果参数全是内置类型,还有必要重载吗?
(5).* :: sizeof ?: . 这5个运算符不能重载(常在笔试题中出现)
5.3 赋值运算符重载
概念
赋值运算符重载和拷贝构造有点相似,拷贝构造是指:一个已经存在的对象,拷贝给另一个要创建初始化的对象;赋值运算符重载是指:一个已经存在的对象,拷贝赋值给另一个已经存在的对象。
(简单来说,赋值运算符重载指:针对两个已经实例化好的对象;拷贝构造是指:在创建对象时用已经存在的对象去初始化)
我们平时可以用一个变量赋值给另一个变量,例如:
int a = 1, b = 2, c = 3;
a = b = c;
如果想让自定义类型的对象也能够做到这一点,就需要使用赋值运算符的重载函数
赋值运算符的重载函数格式如下:
- 参数类型:const 类名 & 参数名,用引用可以减少拷贝,提高传参效率
- 返回值类型:类名&,返回的对象出了作用域不销毁,所以选择传引用返回。返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测*this是否和形参一样,避免自己给自己赋值
- 返回*this,因为要支持连续赋值,保持运算符的特性
现在,我们可以按照上面的格式写一个日期类的赋值运算符重载函数了
小贴士:
赋值运算符重载是类的6大默认成员函数之一,所以它有着一些独特的特性
(1)赋值运算符只能重载为类的成员函数,不能在类外实现
为啥呢?
这是因为,赋值重载函数是默认成员函数,如果我们不实现,编译器就会自己生成一个。
此时如果我们再去类外去实现一个全局的赋值运算符重载,就和编译器在类中实现的赋值重载冲突了,所以赋值重载函数只能在类中实现。
(2)当用户没有显式实现赋值运算符重载时,编译器会生成一个默认的,并逐字节拷贝
对于内置类型的成员变量直接拷贝即可,对于自定义类型的成员变量,则需要调用对应类的赋值运算符重载。
可以看到,在调用Date类的默认赋值重载函数时,对于自定义类型的成员变量_t,会去调用它的赋值运算符重载。
和拷贝构造类似,如果类中没有涉及到资源管理,我们就可以不实现赋值重载;一旦涉及资源管理就必须要自行实现。
5.4 热身一下:
刚刚我们学习了赋值运算符重载,那么动动你的小手指,把这些赋值重载的函数都实现一下吧~相信聪明的你肯定是没问题的!
我们想要实现 大于(>)、大于等于(>=)、小于(<)、小于等于(<=)、等于(==)、不等于(!=) ,这些运算符重载,可以写一个函数声明和函数定义,声明和定义分离,这样更清晰一点。
Date.h
#pragma once
#include<iostream>
using namespace std;
class Date {
public:
//缺省参数只在声明时使用
Date(int year = 2024, int month = 9, int day = 5);
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);//不等于
private:
//成员变量私有化
int _year;
int _month;
int _day;
};
Date.cpp
注意啦!在Date.cpp中写函数时,一定要加上作用域限定符::,否则编译器是找不到的哟~
#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 (this->_year < d._year) {
return true;
}
else if (this->_year == d._year) {
if (this->_month < d._month) {
return true;
}
else if (this->_month == d._month) {
return this->_day < d._day;
}
}
return false;
}
//小于等于运算符重载
bool Date::operator<=(const Date& d) {
return (*this < d || *this == d);
//或者 return !(*this > d)
}
//大于运算符重载
bool Date::operator>(const Date& d) {
return !(*this <= d);
}
//大于等于运算符重载
bool Date::operator>=(const Date& d) {
return (*this > d ||*this == d);
//或者 return !(*this < d)
}
//等于运算符重载
bool Date::operator==(const Date& d) {
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
//不等于运算符重载
bool Date::operator!=(const Date& d) {
return !(*this == d);
}
我们不难发现,只要我们写了等于运算符函数,大于运算符或者小于运算符重载函数, 其他的运算符函数都可以复用它们。因此,看似那么多的运算符重载函数,实质上,只需要完整写2个就可以啦~
5.5 小练习1
我们知道,"日期"+"日期"没有任何任何意义,而日期+天数 --> 日期,假设今天是2024年8月15日,如果我们想加上50天,得到新的日期,该怎么做呢?
①我们先用今天的日期+50天,也就是15+50=65。
②8月有31天,再用65-31=34天。也就是说,8月过完了还有34天剩余。
③9月有30天,再用34-30=4天。9月过完了还有4天剩余。
④现在是10月,从10月1号开始计算,10月4号结束。最终的答案为10月4号。
我们可以先写一个获取日期的函数
//直接定义类里面,它默认是inline
int GetMonthDay(int year, int month) {
//保证month的合法
assert(month > 0 && month < 13);
//1-12月份,为了方便,从下标为1的位置开始存数据
int MonthArray[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;
}
//如果不是2月,直接返回
else {
return MonthArray[month];
}
}
接着,我们可以实现关于日期类的赋值运算符重载函数
Date.h 里面的函数声明
//注意:这里的运算符是+=
Date& operator+=(int day);
//注意:这里的运算符是+
Date operator+(int day);
Date.cpp 里面的函数定义
Date& Date::operator+=(int day) {
//先加上天数
_day += day;
//如果剩余的天数大于该月的天数,进入循环
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);//日期更新
_month++; //月份更新
if (_month == 13) //代表这一年已经走完了
{
_year++; //年份更新
_month = 1; //从1月开始
}
}
return *this;//最后返回*this;
}
Date Date::operator+(int day) {
Date temp = *this;//将临时变量temp存储*this
temp._day += day; //先加上天数
//如果剩余的天数大于该月的天数,进入循环
while (temp._day > GetMonthDay(temp._year, temp._month)){
temp._day -= GetMonthDay(temp._year, temp._month);//日期更新
temp._month++;//月份更新
if (temp._month == 13) //代表这一年已经走完了
{
temp._year++; //年份更新
temp._month = 1; //从1月开始
}
}
return temp;//返回临时变量,变量出作用域被销毁,因此不能传递引用
}
//注意:这里的运算符是+
//第二种方法也可以这样写
Date Date::operator+(int day) {
Date temp = *this;//将临时变量temp存储*this
temp += day; //复用Date Date::operator+=(int day);
return temp; //将temp返回
}
刚刚我们练习了日期+天数,那么,日期-天数该如何实现呢?
还是和之前一样,我们一起来分析分析~
假设今天是2024年8月15日,如果我们往前计算50天,那么日期为多少呢?
①我们先用今天的日期-50天,也就是15-50=-35,8月算完了还欠35
②7月总共有31天,31-35=-4,7月算完了还欠-4
③6月总共有30天,30-4=26。相当于从6月30号开始倒着计算,扣4天,最终答案为2024年6月26日。
我们可以写一个关于日期-天数的赋值运算符重载:
Date.h 里面的函数声明
//注意:这里的运算符是-=
Date& operator-=(int day);
//注意:这里的运算符是-
Date operator-(int day);
Date.cpp 里面的函数定义
//注意:这里的运算符是-=
Date& Date::operator-=(int day) {
//1.先将天数减到日期上面
_day -= day;
//_day为负的时候,则借位到上一个月份
//_day为0的时候,还是借位到上一个月份
//当_day大于0的时候,退出循环
while (_day <= 0) {
//如果此时日期为负,则借位到上一个月份
_month--;
//如果此时_month为0,则借位到上一个年份
if (_month == 0) {
_year--; //年份更新
_month = 12;//月份更新
}
//借上一个月的天数
_day += GetMonthDay(_year, _month);
}
return *this;//返回*this
}
或者这样:
//注意:这里的运算符是-
Date Date::operator-(int day) {
Date temp = *this; //定义临时变量temp,存储*this
temp._day -= day; //先将天数减到日期上面
//当temp._day大于0的时候,退出循环
while (temp._day <= 0) {
temp._month--; //如果此时日期为负,则借位到上一个月份
if (temp._month == 0) //如果此时_month为0,则借位到上一个年份
{
temp._year--; //年份更新
temp._month = 12; //月份更新
}
//借上一个月的天数
temp._day += GetMonthDay(temp._year, temp._month);
}
return temp;//返回临时变量temp,temp出作用域就被销毁,不能传引用&
}
还可以这样:
//注意:这里的运算符是-
Date Date::operator-(int day) {
Date temp = *this;//定义临时变量temp,存储*this
temp -= day; //复用Date::operator-=(int day)函数
return temp; //返回临时变量temp
}
测试一下代码:
ps:如果给 Date Date::operator+=(int day)函数传递过来的day为负数,怎么办呢?
很简单,我们先在函数里面做一个判断,如果传递过来的day为负数,则复用 Date Date::operator-=(int day)函数。因为传递过来的day为负数,所以我们需要再day前面添加一个“-”号,保证day为正数。
Date& Date::operator+=(int day) {
//如果传递过来的day<0
//复用Date& Date::operator-=(int day)函数
//将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; //从1月开始
}
}
return *this;//最后返回*this;
}
同理, 如果给 Date Date::operator-=(int day)函数传递过来的day为负数,我们可以在函数内进行判断,看看是否调用 Date Date::operator+=(int day)函数。
//注意:这里的运算符是-=
Date& Date::operator-=(int day) {
//如果传递过来的day为负数
//负负得正,调用Date& Date::operator+=(int day)函数
//因为day为负数,我们需要在day前面添加一个"-"号
if (day < 0) {
return *this += -day;
}
//1.先将天数减到日期上面
_day -= day;
//_day为负的时候,则借位到上一个月份
//_day为0的时候,还是借位到上一个月份
//当_day大于0的时候,退出循环
while (_day <= 0) {
//如果此时日期为负,则借位到上一个月份
_month--;
//如果此时_month为0,则借位到上一个年份
if (_month == 0) {
_year--; //年份更新
_month = 12;//月份更新
}
//借上一个月的天数
_day += GetMonthDay(_year, _month);
}
return *this;//返回*this
}
你可能会问:函数重载和运算符重载有什么联系呢?
实际上,它俩一点关系都木有!
函数重载:可以让函数名相同,参数不同的函数存在
运算符重载:让自定义类型可以用运算符,并且控制运算符的行为,增强可读性。
多个同一运算符重载可以构成函数重载
5.6 前置++和后置++重载
前置++的重载函数
前置++:返回+1之后的结果
//前置++的重载函数
Date& operator++() {
*this += 1;
return *this;
}
注意:this指向的对象在函数结束后不会销毁,所以可以使用传引用返回提高效率
后置++的重载函数
后置++:前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
//后置++的重载函数
Date operator++(int) {
Date temp(*this);
*this += 1;
return temp;
}
因为后置++是先使用后+1,要返回+1之前的旧值,所以需要temp来保存*this的值,然后给*this+1。
temp出了作用域就会销毁,所以只能传值返回
后置++重载函数中的参数int没有实际作用,只是为了与前置++构成函数重载,以便区分
前置++和后置++的使用:
遇到后置++重载函数时,编译器会自动在参数中放一个整型用来匹配函数。
我们知道怎么写前置++和后置++重载,那么我相信,前置--和后置--也难不到你~
前置--的重载函数
//前置--的重载函数
//--d1
Date& Date::operator--() {
*this -= 1;
return *this;
}
后置--的重载函数
//后置--的重载函数
//为了区分前置--和后置--,构成函数重载,给后置--强行增加了int形参
Date Date::operator--(int) {
//用temp存储*this
Date temp(*this);
*this -= 1;
return temp;
}
5.7 小练习2
好啦,我们了解日期加一个天数或者日期减一个天数,那么怎么计算2个日期之间的天数呢?
思路:让较小的日期追上较大的日期即为2个日期之间的天数。
我们可以假设*this为较大的日期,d为较小的日期,定义标识flag为1,表明此时 *this-d>0,(后面可以进行更改),定义变量n,n从0开始计数,较小的日期追上较大的日期时,n停止计数。最后返回 n*flag的值。
Date.h里面的函数声明
//计算2个日期相差的天数
//d1-d2
int operator-(const Date& d);
Date.cpp里面的函数定义
//计算2个日期相差的天数
//d1-d2
int Date::operator-(const Date& d) {
//假设*this为较大的,d为较小的,flag为1
Date max = *this;
Date min = d;
int flag = 1;
//如果d>*this,那么d为max,*this为min,flag为-1
if (*this < d) {
max = d;
min = *this;
flag = -1;
}
//通过n来计算2个日期相差的天数
//让小的日期追上大的日期
int n = 0;
while (min != max) {
//采用前置++的效率更高
++min;
++n;
//min++;
//n++;
}
return n * flag;//如果d1>d2,那么flag为1,结果为正数
//如果d1<d2,那么flag为-1,结果为负数
}
测试一下:
六、const成员
如果一个被const修饰的对象去调用一个普通的成员函数,因为this指针没有被const修饰,就会造成权限放大。
针对这种情况,我们需要用const对this指针进行修饰,但是this指针是隐式的,该怎么修饰它呢?
只需要在成员函数的后面加上const即可。
我们将const修饰的成员函数称为const成员函数,看上去是修饰成员函数,实际上修饰的是隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们看看下面这段代码:
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 2024, int month = 9, int day = 7) {
_year = year;
_month = month;
_day = day;
}
void Print() {
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const {
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 9, 1);
d1.Print();
const Date d2(2024, 9, 6);
d2.Print();
return 0;
}
其中,对象d1没有被const修饰,对象d2被const修饰,它们都调用了Print函数
而一个Print函数没有被const修饰,一个被const修饰,函数的调用情况如何呢?
可以看到,d1调用了第一个Print,d2调用了第二个被const修饰的Print
Q1:非const对象可以调用const成员函数吗?
我们 将第一个Print删除,运行程序
此时两个对象都能调用const成员函数
Q2:const成员函数内可以调用其他非const成员函数吗?
我们恢复第一个Print,并在第二个Print内调用第一个Print,运行程序
程序正常运行
Q3:非const成员函数内可以调用其他const成员函数吗?
程序正常运行
如果成员函数内部不需要修改成员变量时,都可以在后面加上const,这样普通对象和const对象都可以调用
七、取地址操作符重载
这两个默认成员函数一般不需要我们定义,编译器会默认生成。
它们起到对普通对象和const对象取地址的作用
#include<iostream>
using namespace std;
class Date {
public:
Date* operator&() {
return this;
}
const Date* operator&() const {
return this;
}
private:
int _year;
int _month;
int _day;
};
片尾
今天我们学习了C++的类和对象(中),希望看完这篇文章能对小伙伴们有所帮助~
求点赞收藏加关注!!!
谢谢大家!!!