前言
本节我们继续学习C++中的重要内容——类和对象,本节的内容非常多,而且难度较大,那么废话不多说,我们正式进入今天的学习
1.类的6个默认成员函数
如果一个类中什么成员都没有,那么这个类就被称为空类
但是在空类之中的真实情况并不是什么都没有,任何的一个类在什么都没有写的时候,编译器会自动生成6个默认的成员函数
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数被称为默认成员函数
总结(建议把所有的默认成员函数写完再看):
一:默认生成的构造和析构函数
1.内置类型不做任何处理
2.自定义类型调用默认的构造和析构函数
二:默认生成的拷贝和赋值函数
1.内置类型完成值拷贝(浅拷贝)
2.自定义类型调用拷贝构造赋值重载函数
三:取地址重载函数
2. 构造函数
2.1 构造函数的概念
我们再次使用之前的日期代码来引入构造函数的学习:
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.Init(2024, 5, 16);
d1.Print();
Date d2;
d2.Init(2024, 5, 17);
d2.Print();
return 0;
}
对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
2.2 构造函数的特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象
构造函数的特征如下:
1. 函数名与类名相同
2. 无返回值(普通的函数无返回值需要写void,构造函数不需要写void)
3. 对象实例化时编译器自动调用对应的构造函数
4. 构造函数可以重载
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
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.Print();
return 0;
}
构造函数分为两类:第一类是无参构造函数,第二类是带参构造函数
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 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;
};
int main(void)
{
Date d1;
// 调用无参构造函数
Date d2(2024, 5, 16);
// 调用带参的构造函数
d1.Print();
d2.Print();
return 0;
}
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
5. 构造函数,也就是默认成员函数,如果我们不写则C++编译器会自动生成一个无参的默认构造函数,如果我们写了就不会生成了,自动生成的构造函数对内置类型的成员不会处理(C++11,声明中支持给缺省值,但不是初始化),自定义类型的成员才会处理,会去调用这个成员的构造函数
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
*/
6.关于编译器生成的默认成员函数,很多人可能会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默 认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象中的成员变量_year/_month/_day,依旧是随机值,也就说在这里编译器生成的默认构造函数似乎并没有什么用?
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...(Date* 也是内置类型,因为它本质上是一个指针),自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认构造函数
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 _t;
};
int main()
{
Date d;
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值
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 = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(多个并存会存在调用的二义性) 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可以调用的函数)
我们来看一个例题:以下测试函数能通过编译吗?
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
return 0;
}
答案是不能
总结:一般情况下都需要我们自己书写构造函数,来决定初始化方式。当成员变量全都是自定义类型的时候,可以考虑不写构造函数
3.析构函数
3.1 概念
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么被销毁的呢?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
3.2 特性
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符~
2. 无参数无返回值类型
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。内置类型的成员不会被处理,自定义类型的成员会调用这个成员的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数
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;
}
编译器会在程序运行结束后输出:~Time()
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是: main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date 类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
4. 拷贝构造函数
4.1 概念
在现实生活中,存在着双胞胎。那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?此时我们就需要了解拷贝构造函数
class Date
{
public:
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;
};
void func1(Date d)
{
d.Print();
}
int main()
{
Date d1(2024, 5, 18);
func1(d1);
return 0;
}
我们知道,若是要在C语言中完成形参的拷贝,则它的拷贝形式有点类似于memcpy,就是按照顺序依次拷贝十二个字节(三个int变量)的内容给形参,而且对形参的修改将不会影响实参
这种拷贝形式在一般的情况下是不存在什么问题的,就如上述代码所展示的一样。但是在少数的情况下就会出现问题,例如:在栈中完成拷贝
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void func2(Stack s)
{
}
int main()
{
Stack s1;
func2(s1);
return 0;
}
这是为什么呢?
我们通过调试可以知道,程序崩溃的位置在析构函数free的地方
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
那为什么free会崩溃呢?
其实在C语言的某些情景中中也存在这样的问题,这个问题叫做浅拷贝问题。什么是浅拷贝问题呢?我们来绘制一个图像并说明这个问题:
我们知道,首先在main函数中有一个栈帧叫做s1。随后我们用main函数调用了func2函数,此时就会建立一个func2的栈帧。建立完func2的栈帧以后,我们就要完成传参的操作。传值传参又叫做值拷贝或者浅拷贝,意思是我们直接把s1中的参数的取值拷贝给func2中的s。所以此时就存在着问题,s1中的_array变量指向的是堆上的空间(_top和_capacity的拷贝不存在问题),_array是一个指针变量,所以相当于把s1中的_array变量指向的地址拷贝给了s中的_array变量,此时两个_array变量都指向同一块空间。因为出了作用域对象会自动调用析构函数,因为调用析构函数分先后,func2会先调用析构函数,此时就会把指向的空间释放掉。此时轮到s1了它还是会继续调用析构函数,同一块空间就会被释放两次,所以程序就会崩溃。此情景下C语言不会存在问题,因为C语言不会自动调用析构函数
那么有没有什么办法可以解决这个问题呢?
我们可以加上引用的符号&,此时就不存在浅拷贝的问题
void func2(Stack& s)
但是此时又会出现新的问题,如下述代码所示:
void func2(Stack& s)
{
s.Push(1);
}
int main()
{
Stack s1;
func2(s1);
return 0;
}
如果我们要对s对象进行压栈操作,此时也会影响到s1对象。如果我们的要求是要对s中插入一些数据,但是对s的改变不能够影响s1,此时就无法完成任务
在解决这个问题之前我们需要了解一个函数,这个函数叫做拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用
4.2 特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须是同类型的对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d)
// 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
这里我们需要注意:在写拷贝构造函数的时候一定要写引用符号&,不能写成下面的形式:
Date(const Date d)
我们如果使用传值传参的方式此时就存在问题,编译器认为传参就是一个拷贝构造。我们知道,调用函数的第一步就是要先传参。传参就会形成一个拷贝构造,而拷贝构造又需要先传参,此时又会生成一个拷贝构造,此时就会引发无穷递归
此时可能有人会存在问题:能不能用指针解决问题?
Date(const Date* d)
{
_year = d->year;
_month = d->month;
_day = d->day;
}
答案是可以的,但是最好不使用指针解决问题,因为它在调用时的写法很复杂:
Date d2(&d1);
Func(&d1);
注意:拷贝构造有两种不同的写法,这两种写法的效果是等价的:
Date d2(d1);
Date d2 = d1;
若未显式定义,编译器会生成默认的拷贝构造函数。
1.如果是内置类型,就会自动完成值拷贝
2.如果是自定义的类型,就会调用其他的拷贝
所以说:类似于Date类型的对象不需要我们实现拷贝构造,用编译器自动生成的就行;类似于Stack类型的对象就需要我们自己实现深拷贝的拷贝构造,使用编译器自动生成的就会出现问题
4.3初识深拷贝
刚才我们学习了拷贝构造函数,但是经过拷贝构造函数的处理仍然是一个传值操作,两个对象中的_array仍然指向的是同一块空间,所以仍然无法解决程序崩溃的问题。要想解决这类情况下程序崩溃的问题,我们需要了解深拷贝
我们用深拷贝对对象中的拷贝构造函数进行改造:
typedef int DataType;
class Stack
{
public:
Stack(Stack& s)
{
cout << "Stack(Stack& s)" << endl;
//深拷贝
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
memcpy(_array, s._array, sizeof(DataType) * s._size);
_size = s._size;
_capacity = s._capacity;
}
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void func2(Stack s)
{
}
int main()
{
Stack s1;
func2(s1);
return 0;
}
因为不能让两个_array变量指向同一块空间,所以此时我们额外开辟一个空间,让 s 中的_array变量指向新的空间。我们先开辟一个大小和 s.capacity 一样大的空间,让s中的_array指向这一块空间,再把数据也拷贝到这个空间里面去,最后让 _size 和 _capacity 变量与s中的 _size 和 _capacity 变量相等,此时我们就完成了深拷贝
4.4 总结
1. 拷贝构造函数是构造函数的一个重载形式
2. 拷贝构造函数的参数只有一个且必须是同类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,不需要自己显式实现
5. 拷贝构造函数典型调用场景: 使用已存在对象创建新对象,函数参数类型为同类型对象,函数返回值类型为同类型对象
5.赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator 操作符(参数列表)
用日期类<来加深运算符重载的理解
我们先来设置一个情景:假设我们要对两个日期进行大小的比较
class Date
{
public:
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(void)
{
Date d1(2024, 5, 18);
Date d2(2023, 5, 18);
if (d1 > d2)
{
//...
}
return 0;
}
我们像上述代码那样直接用运算符比较显然是不可以的
我们正确的做法应该是写一个函数去比较日期的大小:
处理方法一:
bool Dateless(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
else
{
return false;
}
}
处理方法二:运算符重载
bool operator<(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
else
{
return false;
}
}
但是如果对象里面的变量是私有的,以上的两种方法都无法奏效
我们可以把这两个函数写成类中的成员函数来解决问题。但是我们在把它写成了成员函数之后我们发现仍然存在问题:
此时编译器报错了,称 operator< 的参数太多了
我们需要知道运算符重载是有相关规定的:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个自定义类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现
6.不能改变操作符的操作数的个数。一个操作符是几个操作数,那么重载的时候就应该有几个参数
根据第4、6点我们知道,有一个参数应该省略
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = 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;
}
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2024, 5, 18);
Date d2(2023, 5, 18);
cout << (d1.operator<(d2)) << endl;
return 0;
}
日期类的完善(==、!=、<=、>=、>)
此时我们还可以写出判断两个日期是否相等的代码:
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
若是我们此时要完成小于等于的代码,此时肯定很多人都会选择把代码重新写一遍,但是这么处理太麻烦了,而且会很浪费时间。我们实际上可以这么处理:
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
我们知道,d表示的就是我们传入的d2对象,而d1的地址是隐藏的 this 指针
此时我们就可以把所有的类型补充完整:
bool operator>(const Date& d)
{
return !(*this <= d);
}
bool operator!=(const Date& d)
{
return !(*this == d);
}
bool operator>=(const Date& d)
{
return !(*this < d);
}
日期类+和+=的实现
我们还可以接着完善代码,我们知道虽然 日期 + 日期 是没有意义的,但是 日期 + 天数 是有意义的,所以我们可以重载一个操作符 += :
1.我们先直接把要加的天数全部加到日期上
2.判断一下当前的日期上的数字是否大于当月的最大天数。如果大于最大天数就先在日期代表的数字上减去当月的最大天数,然后让月所代表的数+1;随后再判断月数是否为13,如果是13就先把月所代表的数字改成1,再让年所代表的数字+1……以此类推,直到所有位置上的数字都符合条件
3.我们还需要考虑是否为闰年,闰年2月有29天
我们先来写一个函数来判断每个月的天数:
int GetMonthDay(int year, int month)
{
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;
}
return monthArray[month];
}
此时我们就可以根据上述的逻辑写出代码:
Date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
//月进位
_day -= GetMonthDay(_year, _month);
++_month;
//月满了
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
int main(void)
{
Date d1(2024, 5, 18);
Date ret = d1 += 50;
ret.Print();
d1.Print();
return 0;
}
此时我们很直观地发现了一个问题:对ret的修改影响到了d1,我们有什么办法能让ret的修改不影响到d1吗?
我们重新定义一个符号+,表示不影响d1的情况下计算天数+日期。要想完成这个任务,我们可以调用拷贝构造,把拷贝出的内容放到一个新的对象tmp中,再对tmp中的成员进行处理,此时它的修改将不会影响到d1.最后我们再 return tmp 就行
因为出了作用域对象就已经不存在了,此时我们就不能使用引用返回:
Date operator+(int day)
{
Date tmp(*this);
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
//月进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
//月满了
if (tmp._month == 13)
{
++tmp._year;
tmp._month = 1;
}
}
return tmp;
}
int main(void)
{
Date d1(2024, 5, 18);
Date ret = d1 + 50;
ret.Print();
d1.Print();
return 0;
}
其实这种写法还是麻烦了,我们可以简化代码:
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
5.2借用日期类的实现引出赋值运算符重载
首先我们需要实现声明与定义分离,我们通过之前的学习知道,我们要在声明中给参数而在定义中不给参数
所以此时我们需要创建三个文件:
Date.cpp —— 实现定义
Date.h —— 实现声明
Test.cpp —— 实现测试
我们要实现日期类,首先需要写一个函数来检查某一年的某个月有几天,这个函数我们刚才写过了,所以在这里不做过多的讲解:
定义(Date.cpp):
int Date::GetMonthDay(int year, int month)
{
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;
}
return monthArray[month];
}
声明(Date.h):
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
我们知道,我们每次对日期进行操作的时候都需要调用这个函数,但是如果每次调用函数都去创建monthArray数组的话可能会影响程序的运行效率,我们可以加上static修饰数组,来保持数组的持久性,这样可以在一定的程度上优化程序
static int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
我们接下来需要检查日期是否合法(Date.cpp):
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (month < 1 || month>12 || day < 1 || day > GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
}
我们写一串代码来检验一下(Test.cpp):
void TestDate1()
{
Date d1(2024, 5, 20);
Date d2;
d1.Print();
d2.Print();
Date d3(2022, 13, 20);
d3.Print();
Date d4(2024, 2, 30);
d4.Print();
}
int main(void)
{
TestDate1();
}
因为日期类在实现的时候只需要完成值拷贝就行,所以我们不需要自己写拷贝构造函数,我们来测试一下(Test.cpp):
void TestDate2()
{
Date d1(2024, 5, 20);
Date d2(d1);
d1.Print();
d2.Print();
}
int main(void)
{
TestDate2();
}
我们在生活中存在这样的一个情景:假设我们一开始定义了一个日期,随后发现定义的这个日期出现了错误,我们接着就需要更改这个日期,那么我们此时就需要用到赋值运算符重载:
Date.h:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
int GetMonthDay(int year, int month);
void Print();
Date operator=(const Date& d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
我此时来思考一个问题:假设我们在 void operator=(const Date& d) 中去掉&会对有影响吗?
答案是不会,这里的情况和拷贝构造函数不同,并不会无限递归
这里的情况是赋值而不是拷贝构造,想要解决这个问题我们需要知道拷贝构造和赋值的区别:
拷贝构造:用一个已经存在的对象去初始化另外一个要创建的对象对象
赋值:将两个已经存在的对象进行拷贝
这上面的操作叫做赋值运算符重载
赋值运算符重载格式:
1.参数类型:const T&,传递引用可以提高传参效率
2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回*this :要复合连续赋值的含义
我们若是仔细观察上述的代码就会发现其实代码还有优化的空间:我们选用的是传值返回,所以我们返回的并不是d1,而是d1的拷贝,此时就会调用一次拷贝构造。因为出了函数的作用域 *this 仍然存在,所以我们可以选择不去调用这一次拷贝构造,此时我们就可以使用引用返回:
Date& operator=(const Date& d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
我们来学习这种语法的一个缺陷,假设我们直接写出 d1 = d1 这样的代码,这种代码在语法上并没有出现错误,但是我们在实际写代码中并不会存在自己给自己赋值这样的情况,所以我们通常采取下面的写法来避免这个操作:
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
我们需要知道,赋值运算符重载也是一个默认成员函数,编译器会自动生成,所以我们即使不去写赋值运算符重载的代码仍然可以实现赋值运算符重载代码的功能
但是类似于Stack类型的函数仍然需要自己写赋值运算符重载函数
注意:赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
学习完了赋值运算符重载,我们现在继续来实现日期类,由于上一小节在引出运算符重载的时候已经完成了一部分,所以那一部分的内容就不做过多的解释,直接复制粘贴过来使用:
我们处理一下,先让声明和定义分离。有的人可能会存在疑问:为什么声明和定义一定要分离呢?因为如果定义全部放在一起的话代码会非常的长,就不方便看类的结构
Date.cpp:
int Date::GetMonthDay(int year, int month)
{
static 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;
}
return monthArray[month];
}
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (month < 1 || month>12 || day < 1 || day > GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
}
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;
}
else
{
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);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _day))
{
_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.h:
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
int GetMonthDay(int year, int month);
void Print();
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
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+=(int day);
Date operator+(int day);
private:
int _year;
int _month;
int _day;
};
假设我们在测试文件中写入:
Date d1(2024, 5, 21);
Date ret = d2;
那么此时调用的是拷贝构造函数还是赋值重载函数呢?
答案是拷贝构造函数,我们先回想一下刚才学习过的:
拷贝构造:用一个已经存在的对象去初始化另外一个要创建的对象对象
赋值:将两个已经存在的对象进行拷贝
其实这种行为取决于编译器的选择,但通常情况下选择调用拷贝构造函数更加合理
所以对于编译器而言:
Date tmp(*this);
Date tmp = *this;
这两种写法是等价的
日期类-=和-的实现
我们接着来完善一下日期类,我们现在来写一下-=的代码:
我们先来了解一下-=的逻辑:
1.若是减去的数小于日期所代表的数字,那么在这种情况下-=比较好实现,只要让日期所代表的数字减去要减的数字就行,年和月保持不变
2.若是减去的数大于日期所代表的数字,那么此时就要进一步分析:我们先要确定那一个月一共有几天,我们先用要减的数字减去日期代表的数字,然后让月所代表的数字-1,接下来判断月的数字是否为0,若为0的话则把月改成12,让年份-1……以此类推,直到要减去的数字为0
Date& Date::operator-=(int 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;
}
我们来测试一下代码(Test.cpp):
void TestDate4()
{
Date d1(2024, 5, 21);
d1 -= 1;
d1.Print();
}
int main(void)
{
TestDate4();
}
此时我们考虑到一个问题,若是被操作的数是一个负数,那么我们按照原代码的处理方式去处理负数时就会出现问题:
void TestDate4()
{
Date d1(2024, 5, 21);
d1 -= -100;
d1.Print();
Date d2(d1);
d2 -= -5;
d2.Print();
Date d3(d1);
d3 += -1;
d3.Print();
Date d4(d1);
d4 += -100;
d4.Print();
}
int main(void)
{
TestDate4();
}
所以我们此时就要对代码进行修改:
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _day))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
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;
}
通过修改,代码正常执行
日期类++的实现
若我们想完成日期的++操作,直接写++会出现问题:
直接写++会出现错误,因为编译器不会自动帮我们实现++,此时我们还是需要用运算符重载来自己实现++
我们在自己实现++的时候就会遇到问题:我们无法区分前置++和后置++,因为这两种模式函数名都应该是++,但是处理方法又存在不同,那么我们该如何处理这种情况呢?
我们来逐步分析:++的操作模式都是相同的,二者的本质区别在于返回值不同。C++之中默认规定
Date operator++()
{}
这种形式默认用于前置++,而后置++为了与前置++区分,C++规定了后置++必须带有一个形式参数,其中形式参数可以给也可以不给,编译器会自动传递
Date operator++(int i)
{}
//或者写作
Date operator++(int)
{}
前置++和后置++既构成运算符重载也构成函数重载
有了这些理论基础我们就可以很快速的写出代码(Date.cpp):
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
tmp += 1;
return tmp;
}
我们来测试一下(Test.cpp)
void TestDate5()
{
Date d1(2024, 5, 21);
++d1;
d1.Print();
Date d2(2024, 5, 21);
d2++;
d2.Print();
}
int main(void)
{
TestDate5();
}
代码运行成功,编写完成
日期类--的实现
--的逻辑和刚才所说的++的逻辑基本相同,我们直接写出代码(Date.cpp):
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
tmp -= 1;
return tmp;
}
我们还是来测试一下(Test.cpp)
void TestDate6()
{
Date d1(2024, 5, 21);
--d1;
d1.Print();
Date d2(2024, 5, 21);
d2--;
d2.Print();
}
int main(void)
{
TestDate6();
}
两个日期相减的实现
我们在日常生活中还会存在用日期相减的情景,例如:算国庆节距离现在还有多少天
此时我们先给出定义(Date.h):
int operator-(const Date& d);
我们细心观察,可以发现这里和之前的日期减去天数构成函数重载
int operator-(const Date& d);
//日期减去日期
Date operator-(int day);
//日期减去天数
要想完成 日期 - 日期 的代码,我们来逐步分析一下
思路一:
1.我们先让待减去的年份与被减去的年份相减,计算出年份的差距
2.我们分别让待减去的年份和被减去的年份分别减去它们各自年份的1月1日
3.我们把2中计算出来的值相减,就可以得出除了年份的天数以外相差的天数
4.我们再让年份的差值乘以365,加上3中计算出来的值,就可以得出差距的天数
该思路实现较为复杂,所以不采用这种思路
思路二:
1.我们不能知道到底是左边的值大还是右边的值大,因为我们的输入是随机的,此时我们就默认假设左边的值大于右边的值,我们令左边的变量为max,令右边的变量为min
2.再来定义一个变量flag,令它的默认取值为1。因为我们并不能确认到底是左边的变量大还是右边的变量大,如果左边的变量大,相减的值就是一个正值;如果右边的变量大,相减的值就是一个负值。所以我们先对左右变量大小进行识别,若是右边的变量更大我们就先把max和min的取值交换,再把flag改成-1;若是左边的变量大则保持不变
3.我们再定义一个变量n,我们让小的变量min不断++,直到等于max变量,同时让n++,现在我们返回 n*flag ,返回的值就是相差的天数
此时我们就可以较为轻易的写出代码(Date.cpp) :
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 n * flag;
}
我们来测试一下(Test.cpp):
void TestDate7()
{
Date d1(2024, 5, 21);
Date d2(2005, 2, 7);
cout << d1 - d2 << endl;
}
int main(void)
{
TestDate7();
}
6.日期类代码整合
Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
static 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;
}
return monthArray[month];
}
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (month < 1 || month>12 || day < 1 || day > GetMonthDay(year, month))
{
cout << "非法日期" << endl;
}
}
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;
}
else
{
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);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
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);
tmp += 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
tmp -= 1;
return tmp;
}
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 n * flag;
}
Date.h
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
int GetMonthDay(int year, int month);
void Print();
Date& operator=(const Date& d)
{
if (this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
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+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
void TestDate1()
{
Date d1(2024, 5, 20);
Date d2;
d1.Print();
d2.Print();
Date d3(2022, 13, 20);
d3.Print();
Date d4(2024, 2, 30);
d4.Print();
}
void TestDate2()
{
Date d1(2024, 5, 20);
Date d2(d1);
d1.Print();
d2.Print();
}
void TestDate3()
{
Date d1(2024, 5, 20);
Date d2(2024, 5, 21);
d1 = d2;
d1.Print();
d2.Print();
}
void TestDate4()
{
Date d1(2024, 5, 21);
d1 -= -100;
d1.Print();
Date d2(2024, 5, 21);
d2 -= -5;
d2.Print();
Date d3(2024, 5, 21);
d3 += -1;
d3.Print();
Date d4(2024, 5, 21);
d4 += -100;
d4.Print();
}
void TestDate5()
{
Date d1(2024, 5, 21);
++d1;
d1.Print();
Date d2(2024, 5, 21);
d2++;
d2.Print();
}
void TestDate6()
{
Date d1(2024, 5, 21);
--d1;
d1.Print();
Date d2(2024, 5, 21);
d2--;
d2.Print();
}
void TestDate7()
{
Date d1(2024, 5, 21);
Date d2(2005, 2, 7);
cout << d1 - d2 << endl;
}
int main(void)
{
TestDate7();
}
7.const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
这串代码在内部会默认转换为:
int main(void)
{
const Date d1(2024, 5, 21);
d1.Print(&d1);
//void Date::Print(Date* this)
//{
// cout<< this->_year << "年" << this->_month << "月" << this->_day << "日" << endl;
//}
return 0;
}
编译器会默认给 Print( ) 函数中提供参数 &d1 (&是取地址,不是引用):
int main(void)
{
const Date d1(2024, 5, 21);
d1.Print(&d1);
return 0;
}
而该参数的类型是 const Date* ,const指向的内容是不允许改变的。我们知道,成员函数 Print( ) 的形式参数的类型是 Date* ,当实际参数传递给形式参数的时候会发生权限的放大,此时就会报错
void Date::Print(Date* this)
{
cout<< this->_year << "年" << this->_month << "月" << this->_day << "日" << endl;
}
要想解决const成员的打印的问题,我们必须把 Print( ) 函数中的形式参数类型更改为 const Date*这样才能避免权限放大的问题。C++中针对这一问题规定:在 Print( ) 函数的后面加上const就能实现对this指针的修饰
void Date::Print() const
{
cout<< this->_year << "年" << this->_month << "月" << this->_day << "日" << endl;
}
我们来看一下下面这个问题,当我们在打印函数的后面加上const以后,d2中的打印函数能够成功调用吗?
int main(void)
{
Date d2(2024, 5, 21);
d2.Print();
return 0;
}
答案是能,这里是权限的缩小,我们通过之前的学习可以知道:权限是可以平移和缩小的,但是权限不可以放大
此时可能会有人存在疑问:我们既然已经知道了const后置的意义,那么const前置有什么意义呢?
举个例子:
const int func()
{
int ret;
return ret;
}
这里的const修饰的是返回值,其实这里加const与不加const在本质上是相同的。因为这里是传值返回,返回的并不是ret这个变量,而是ret变量的拷贝,是一个临时对象,而临时对象具有常性,是不能被修改的
当函数是用于引用返回的时候const前置才有意义,能够避免返回值被修改:
const int& func()
{
static int ret;
return ret;
}
接下来我们来看一个问题,请问这两个函数能否同时存在:
void Print() const;
void Print();
答案是可以同时存在,因为这两个函数构成函数重载,编译器认为二者类型不同,一个类型为const Date* ,另外一个类型为 Date*
这两者同时存在的意义在于:在执行 Print( ) 函数的时候,编译器会自动选择最匹配的Print( ) 函数
8.取地址及const取地址操作符重载
这两个默认成员函数一般不用自己定义 ,编译器默认会自动生成
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
只有一种情况下需要自己写,但是这种情况非常特殊:不想被取到有效的地址时需要自己写
Date* operator&()
{
return nullptr;
}
const Date* operator&()const
{
return nullptr;
}
结尾
本节所学习的知识非常重要,而且本节的内容较多、难度较大,需要我们好好的接收、理解、运用,希望可以给你带来帮助,谢谢您的浏览!!!