前言
在上一篇博文,我们了解类中有成员变量和成员函数。在本篇博文,将会详细介绍类的默认成员函数。
我们来看下面这个类
class A
{
}
空类中真的什么都没有吗?并不是,类在创建时,编译器会自动生成以下6个默认成员函数。
类的默认成员函数是用户在创建类时隐式生成的。
一、构造函数
🌽 概念
构造函数是类的一种特殊成员函数,用于初始化对象的状态。构造函数在对象创建时被自动调用,其主要目的是给对象的成员变量赋初值和进行必要的资源分配。并且在对象整个生命周期内只调用一次。
#include<iostream>
using namespace std;
class Date
{
public:
int year;
int month;
int day;
//构造函数
//(1)无参构造函数
Date()
{
this->year = 2;
this->month = 2;
this->day = 2;
}
//(2)有参构造函数
Date(int year , int month , int day )
{
this->year = year;
this->month = month;
this->day = day;
}
public:
void Init(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
void Init()
{
this->year = 2;
this->month = 2;
this->day = 2;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
//没有传递参数,自动调用无参构造函数
Date d1;
//自动调用有参构造函数
Date d2(2,3,4);
d1.Print();
//out: 2年2月2日
d2.Print();
//out: 2年3月4日
return 0;
}
🥕 特性
(1)函数名和类相同
(2)构造函数无返回值(无需写void
)
(3)实例化对象自动调用对应类的构造函数
(4)构造函数支持重载
#include<iostream>
using namespace std;
class Date
{
public:
int year;
int month;
int day;
Date()
{
this->year = 5;
this->month = 5;
this->day = 5;
}
Date(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
//调用无参构造函数
Date d1;
//有参构造函数
Date d2(4, 6, 9);
d1.Print();
d2.Print();
return 0;
}
d1对象调用Date类无参构造函数,d2对象调用Date有参构造函数。
注意:使用类实例化对象时不需要加(),否则会和返回类型为该类的函数混淆。
Date d1(); //错误,容易和函数声明混淆
Date d1;//正确
(5)只有当用户没有显式定义构造函数,编译器才会自动生成一个无参构造函数。
-
当显式定义一个有参构造函数
由于用户定义了有参构造函数,编译器没有自动生成无参构造函数,因此在创建对象d1时不能以无参的形式初始化。 -
当无显式定义构造函数
#include<iostream>
using namespace std;
class Date
{
public:
int year;
int month;
int day;
//构造函数
//(1)无参构造函数
/*Date()
{
this->year = 2;
this->month = 2;
this->day = 2;
}*/
//(2)有参构造函数
/*Date(int year , int month , int day )
{
this->year = year;
this->month = month;
this->day = day;
}*/
public:
void Init(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
void Init()
{
this->year = 2;
this->month = 2;
this->day = 2;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
//没有传递参数,自动调用无参构造函数
Date d1;
d1.Print();
//d2.Print();
return 0;
}
很明显,我们没有传入值到类中。因此当使用编译器的无参构造函数,类的内置类型不会被初始化。
(6)编译器自动生成的默认构造函数,对内置类型的对象不做处理,只调用自定义类型对象构造函数。
#include<iostream>
using namespace std;
class A
{
private:
int x;
int y;
public:
A()
{
cout << "hhh" << endl;
}
};
class Date
{
public:
//内置类型
int year;
int month;
int day;
//自定义类型
A a;
public:
void Init(int year, int month, int day)
{
this->year = year;
this->month = month;
this->day = day;
}
void Init()
{
this->year = 2;
this->month = 2;
this->day = 2;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
Date d1;
d1.Print();
return 0;
}
创建了一个A类
当创建基于Date类的对象d1时,使用了编译器生成默认无参构造函数,去调用了自定义类型 A类 的构造函数。
默认无参构造函数不处理内置类型,所以在打印Date类中的内置类型year
,month
,day
才会出现随机值。
- 内置类型
C++语言自带的数据类型,如基本数据类型,指针类型,引用类型,数组类型等等
- 自定义类型
自定义类型是程序员根据特定需求定义的类型,主要包括类,结构体,联合,枚举等等。
注意:C++11针对使用用构造函数内置类型无法初始化这个问题,打了补丁,即内置类型的成员变量在声明时可以给默认值。
#include<iostream>
using namespace std;
class Date
{
public:
int year;
int month;
int day;
//无参构造函数
//Date()
//{
// this->year = 5;
// this->month = 5;
// this->day = 5;
//}
//全缺省构造函数
Date(int year=5, int month=5, int day=5)
{
this->year = year;
this->month = month;
this->day = day;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
Date d1;
Date d2(4, 6, 9);
d1.Print();
d2.Print();
return 0;
}
全缺省构造函数中实现了无参构造函数的功能——内置类型初始化。
(7)无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
被认为是默认构造函数的有三种:
- 显式的无参构造函数
Date()
{
this->year = 1;
this->month = 1;
this->day = 1;
}
- 显式的全缺省构造函数
Date(int year=1, int month=1, int day=1)
{
this->year = year;
this->month = month;
this->day = day;
}
- 编译器默认生成的构造函数
注意:默认构造函数是指不传参就可以调用的构造函数
比如上面提到的无参构造函数和全缺省函数。
总结:一般情况下构造函数都需要我们显式的实现,只有少数场景可以让编译器自动生成
- 由于构造函数对内置类型的对象不做处理,只调用自定义类型对象构造函数,而自定义类型本质上也是内置类型。
- 当类内部成员都是自定义类型时,可以让尝试编译器自动生成
二、析构函数
🍸 概念
析构函数负责在对象生命周期结束时自动执行清理工作,功能恰好和构造函数相反。
那么清理工作具体是什么呢?
- 释放对象在构造和使用过程中分配的资源(如动态内存、文件句柄、网络连接等)
- 调用对象中其他基类生成对象的析构函数(在派生类中,析构函数会隐式或显式地调用基类的析构函数,以确保基类部分也得到适当清理)
🍹 特性
(1)析构函数名是在类名前加上字符 ~
(2)析构函数无参数无返回类型(不需要写void
)
(3)一个类只能有一个析构函数,如果用户没有显式定义,编译器会自动生成默认的析构函数。
(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 对象离开作用域
#include<iostream>
using namespace std;
class Date
{
public:
int year=8;
int month=9;
int day=10;
Date(int year=1, int month=1, int day=1)
{
this->year = year;
this->month = month;
this->day = day;
}
~Date()
{
cout << "~Date" << endl;
if (year||month||day)
{
year = 0;
month = 0;
day = 0;
}
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
void func()
{
Date d;
}
int main()
{
func();
return 0;
}
func函数内部创建了Date类的对象d,func函数调用结束,对象d离开了作用域,被析构函数释放资源
- 关闭文件句柄
#include <iostream>
#include <fstream>
class FileHandler {
private:
std::ofstream file;
public:
FileHandler(const std::string& filename) {
file.open(filename);
std::cout << "File opened.\n";
}
~FileHandler() {
if (file.is_open()) {
file.close();
std::cout << "File closed.\n";
}
}
};
int main() {
FileHandler file("example.txt");
// ...
return 0;
}
析构函数确保打开的文件在对象生命周期结束时被关闭。
- 调用基类析构函数
#include <iostream>
class Base {
public:
~Base() {
std::cout << "Base class cleanup.\n";
}
};
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived class cleanup.\n";
}
};
int main() {
Derived obj;
return 0;
}
(5)析构函数只需要对有资源申请的类进行处理。(自定义类型成员)
Q:如何理解有资源申请?
这些资源不是简单的数据类型(如int、float、double等),它们的生命周期需要被手动管理。如果这些资源没有被适当地释放,就可能导致资源泄漏。
#include<iostream>
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time" << endl;
}
};
class Date
{
public:
int year=8;
int month=9;
int day=10;
Time t;
Date(int year=1, int month=1, int day=1)
{
this->year = year;
this->month = month;
this->day = day;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
Date d;
return 0;
}
Date类中有自定义成员Time类的对象t,所以在销毁对象d时需要先销毁成员变量Time类的对象t。
编译器会在Date类中生成一个默认析构函数,这个函数再去调用Time类的析构函数。
由于year month day 成员变量是内置类型,析构函数不需要对其进行处理(编译器会自动清理)
所以当类内部没有资源申请(如只有内置类型),则不需要显式调用析构函数,编译器会自动清理。
🥂 析构顺序辨析
我们来看下面这个代码
#include<iostream>
using namespace std;
class A
{
public:
A(int x=1)
:_x(x)
{
cout << "A(int x)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _x;
};
A func()
{
static A a;//函数局部的静态只有第一次调用才构造,生命周期是全局的
return a;
}
A a3;//全局对象在进入main函数之前构造
int main()
{
//后定义先析构
A a1;
A a2;
func();
func();
return 0;
}
问题来了,谁先构造谁先析构呢?
- 通过测试,整个程序进行了四次构造,对象
a3
第一个构造,a1a2
和函数的静态对象aa
逐一构造。整个程序进行了六次析构,顺序为:两次返回临时对象析构,a2a1
析构,静态对象aa
析构,最后是全局对象a3
析构。
//构造顺序:
1)a3
2)a1
3)a2
4)aa
//析构顺序:
1)两次返回临时对象
2)a2
3)a1
4)aa
5)a3
☑️总结:
- 全局对象在进入main函数前被构造
- 局部静态对象只有第一次调用才会发生构造
- 对象后定义先析构
三、拷贝构造函数
🥝 概念
在前面的学习,我们知道了变量可以用其他变量来初始化
int a = 1;
int b = a;
那么对于对象,我们也能通过这种方式初始化吗?拷贝构造函数就是为了解决这个问题的。
拷贝构造函数会创建一个新对象,并用另一个同类型的对象初始化,生成一个新的对象副本。
拷贝构造只有一个参数,规定使用引用传参(一般用const修饰),使用已存在的类类型对象创建时编译器自动调用。
🍅 特性
C++规定自定义类型的传值传参必须调用拷贝构造。
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数只有一个参数,并且必须是当前类对象的引用
class Time
{
private:
int year;
int month;
int day;
public:
//拷贝构造函数(只能使用引用传参,传值传参会出现无限递归问题)
//Time(const Time t) //错误写法
Time(const Time& t)//正确写法
{
this->year = t.year;
this->month = t.month;
this->day = t.day;
}
};
当使用传值调用会造成无限递归调用,编译器会报错。
无限递归调用原因
如果拷贝构造函数的参数是通过值传递的,那么每次调用拷贝构造函数时,都会创建一个新的对象副本,这个副本又需要调用拷贝构造函数来创建它的副本,如此反复,形成无限递归。
使用引用传参就不会造成无限递归调用原因
使用引用传递方式,拷贝构造函数接收的是一个已经存在的A对象的引用,而不是一个新的副本。这样,拷贝构造函数就不需要再次创建一个新的对象副本,从而避免了无限递归。
每次创建新的对象,拷贝构造函数都会自动创建一个该对象的副本
- 拷贝构造函数的参数需要使用const修饰符
(1)保证参数对象不被修改
通过使用const修饰,将无法从拷贝构造函数中修改参数对象。(拷贝构造函数的目的是创建一个对象的副本,而不是改变原有对象的状态。通过将参数声明为const类型,可以确保函数内不会对传入的对象进行任何修改)
(2)允许常量对象作为参数
如果拷贝构造函数的参数不是const引用,那么它只能接受非常量对象作为参数。如果不使用const修饰符,传入常量对象会造成权限放大。
#include<iostream>
using namespace std;
class Time
{
private:
int year;
int month;
int day;
public:
Time(int year=1,int month=2,int day=3)
{
this->year = year;
this->month = month;
this->day = day;
}
Time(Time& t)
{
this->year = t.year;
this->month = t.month;
this->day = t.day;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
int a = 1;
int b = a;
const Time t1(8,9,10);
//Time t2 = t1;
//这里会报错 常量对象t1无法赋值给非常量对象t2,会造成权限放大
return 0;
}
4. 若拷贝构造函数未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
我们把上面显示定义的拷贝构造函数删除,观察编译器是否生成了默认拷贝构造函数
#include<iostream>
using namespace std;
class Time
{
private:
int year;
int month;
int day;
public:
Time(int year=1,int month=2,int day=3)
{
this->year = year;
this->month = month;
this->day = day;
}
void Print()
{
cout << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
};
int main()
{
Time t1(8,9,10);
Time t2 = t1;
t1.Print();
t2.Print();
return 0;
}
可以看到调用了编译器生成的默认拷贝构造函数完成了Time类内置类型的拷贝
因为日期类中没有涉及资源申请,只有内置类型,因此使用编译器生成的默认构造函数就可以完成拷贝。(这种拷贝被称为值拷贝或浅拷贝)
- 涉及资源申请的类需要使用深拷贝
不同于上述的日期类,当类中涉及到资源申请,如栈这种数据结构,需要我们显式的去完成深拷贝。
- 不使用深拷贝会怎么样?
以栈举例
存在栈st1,当使用浅拷贝引用创建st2,栈st1和st2指向同一块内存空间。当程序退出时,st1和st2分别调用它们的destory函数,这会导致释放同一块内存空间多次,导致程序崩溃
- 深拷贝有什么不同?
深拷贝不仅复制对象的非静态成员变量的值,还为指针或动态分配的内存分配新的内存,并复制指针指向的数据到新分配的内存中。这样,新旧对象的指针指向不同的内存区域,互不影响。
- 如何实现深拷贝?
(1)自定义拷贝构造函数:在拷贝构造函数中,为每个指针成员分配新的内存,并复制数据到新的内存中。
以栈举例
typedef int DataType;
class Stack
{
public:
//显式手动的分配内存
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!");
return;
}
_capacity = capacity;
_size = 0;
}
Stack(const Stack& s)
{
_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!");
return;
}
memmove(s._array, _array, sizeof(DataType) * s._size);//复制对象副本
_capacity = s._capacity;
_size = s._size;
}
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;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
(2)使用智能指针:如std::unique_ptr或std::shared_ptr,这些智能指针类型自动管理内存,可以简化资源管理并减少内存泄漏的风险。(后续深入研究,了解即可)
四、赋值运算符重载
🏹 运算符重载
为了使代码更易读和更易于维护,C++引入了运算符重载。运算符重载是拥有特殊函数名的函数,函数名为关键字operator
,该函数和普通函数一样有返回类型。
例如我要对=进行重载,赋值后返回被赋值的变量。
返回值类型 operator=(参数列表)
作用
运算符重载是允许开发者重新定义或“重载”现有运算符(如 +, -, *, /等)以用于自定义数据类型的过程。重载后,这些运算符可以直接用于自定义类型的实例,就像它们用于内置数据类型一样。
注意
(1)使用operator时只能对已经存在的运算符进行重载
例如
operator$
就不行,因为不存在$运算符
(2)重载操作符必须有一个类类型参数(因为重载运算符的目的是用于自定义类型的使用)
(3)作为类成员函数重载时,形参比操作数数目少1,因为成员函数的第一个参数为隐藏的this
使用举例
//重载==运算符,对两个自定义类型进行比较,只需要传入一个类对象的参数即可
bool operator==(const Date & d)//尽量使用引用传入
{
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
重载成全局,无法访问私有成员
1)提供这些成员的get 和 set
2)使用友元
3)重载为成员函数(一般使用这种方法)
在类外部使用友元来访问私有的成员变量
(4)重载后的运算符,其特性应该和重载前一致
(5)以下五个运算符不能重载
?:
(三元运算符).
(引用对象成员)::
(域的解析符)sizeof
(获取数据在内存中所占用的存储空间).*
(引用指向类成员的指针)
🎣 赋值运算符重载
(1)重载格式
- 参数类型:
const 类名&
,使用引用减少拷贝,提高传参效率 - 返回值类型:
类名 &
,返回对象出作用域不销毁(没有调用析构函数),使用引用&,否则只能用传值类名
能用传引用就用传引用,传引用无需拷贝,传值需要拷贝(复制)
- 检测
*this
是否和参数地址完全相同 *this
支持连续赋值,保持运算符的特性
//重载!=运算符
bool operator!=(const Date& d)
{
return !(this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day);
}
//重载=运算符
Date &operator=(const Date& d)
{
//检测*this地址和d是否完全相同,避免自己给自己赋值
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//this是指向当前类对象的指针
return *this;
}
(2)特性
- 赋值运算符只能在类内部重载为类的成员函数,不能在类外实现
在全局使用operator没有this指针,所以需要传入多一个参数
operator必须是非静态成员
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
对于内置类型编译器会自动拷贝,对于自定义类型则调用对应类的赋值运算符重载
- 和拷贝构造函数类似,如果类中未涉及到资源申请,赋值运算符是否实现都可以。一旦涉及到资源申请则必须要显式实现
🏑 前置++和后置++
(1)前置++重载
//前置++ 使用传引用提高效率
Date& operator++()
{
_day++;
return *this;
}
(2)后置++重载
后置++先赋值再自增,用*tmp拷贝原来的对象,对象自增后返回 *tmp
//后置++
Date& operator++()
{
Date* tmp = this;
_day++;
return *tmp;
}
🥊 流运算符重载
(1)概念
流运算符<<(左移)和>>(右移)主要用于I/O操作。左移运算符用于输出,而右移运算符用于输入。在C++标准库中,它们已经被重载,用于处理各种内置类型。
但是对于自定义的数据类型(一般为类或结构体),则需要开发者显式的重载。
operator>>和operator<<可以重载为成员函数
d1<<cout;
但是用起来不符合正常逻辑,不建议这样处理,建议重载为全局函数(下面的举例均重载为全局函数)
(2)重载输入运算符>>
istream 是C++标准库中的一个类,代表输入流。函数返回istream的引用(istream&
),istream &in
是输入流对象,Date& d
是我们自定义的类对象。返回in支持链式操作。
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
(3)重载输出运算符<<
ostream 是C++标准库中的一个类,代表输出流。返回out支持链式操作。
ostream& operator << (ostream& out, const Date & d);
{
out<<d._year<<d._month<<d._day;
return out;
}
注意:iostream和ostream不支持拷贝构造
(4)问题和反思
- 为什么使用引用&返回?
不加引用是值传递,会重新拷贝一份,此时的out或in不再是使用者传递的对象,导致编译器无法正确识别和重载该运算符。使用引用能避免对象的复制并且能够保证参数的唯一性。
- 为什么istream不能加const,ostream可以加const?
- istream不能加const,因为输入操作会改变流的状态,需要修改流的内部状态。
(istream的主要职责是从流中读取数据,这个过程会改变流的状态(如流的缓冲区、读取位置等)- ostream可以加const,因为输出操作不需要修改流的内部状态。
五、日期类的实现
.h文件
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
#define endl "\n"
class Date
{
private:
int _year;
int _month;
int _day;
public:
//流运算符重载(使用友元使能在类外部访问类的私有变量)
friend ostream& operator << (ostream& out, const Date & d);
friend istream& operator >> (istream& in, Date& d);
//为什么istrean不能加const :输入运算符通常需要修改对象的状态,因为它要从输入流中读取数据并将其存储在对象中
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!checkdate())
{
cout << "日期不合法!" << endl;
}
}
bool isleapyear(int year)
{
return year % 400 == 0 || year % 100 != 0 && year % 4 == 0;
}
int getday(int year, int month)
{
assert(year >= 1);
assert(month >= 1 &&month<=12);
//静态数组,相当于放在全局
static int mon[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (isleapyear(year))
{
if (month == 2) return 29;
}
return mon[month];
}
int getday(const Date& d)
{
assert(d._year >= 1);
assert(d._month >= 1 && d._month <= 12);
//静态数组,相当于放在全局
static int mon[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (isleapyear(d._year))
{
if (d._month == 2) return 29;
}
return mon[d._month];
}
void Print();
bool checkdate();
//能写const就写const,这样const对象和非const对象都能调用
//但并不是所有成员函数都能加->修改类的成员数据的不能加const
//非赋值运算符重载
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
bool operator>(const Date& d) const;
bool operator<(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<=(const Date& d) const;
//日期减日期
int operator-(const Date& d) const;
//无需赋值运算符重载,编译器自动生成
//显式实现
Date& operator+(int day) const;
Date& operator-(int day) const;
Date& operator+=(int day);
Date& operator-=(int day);
//day前置++
Date& operator++();
//day前置--
Date& operator--();
};
.cpp文件
#include "Date.h"
//判断日期是否合法
bool Date::checkdate()
{
if (_month > 12 || _month < 1 || _year < 1 || _day<1 || _day>getday(_year, _month))
{
return false;
}
return true;
}
//初始化(全缺省构造函数)
//Date::Date(int year=1,int month=1,int day=1)
//{
// _year = year;
// _month = month;
// _day = day;
//
// if (!check())
// {
// cout << "日期不合法!" << endl;
// }
//}
void Date::Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
//最后一个const允许类的自定义运算符重载在类外部实现
//重载==运算符
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//重载!=运算符
bool Date::operator != (const Date & d) const
{
return !(*this == d);
}
//重载>运算符
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
{
if (_day > d._day)
return true;
else false;
}
else
return false;
}
else
return false;
}
bool Date::operator>=(const Date& d) const
{
if (*this == d || *this > d)
return true;
else
return false;
}
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
bool Date::operator<=(const Date& d) const
{
if (*this == d || *this < d)
return true;
else
return false;
}
Date& Date::operator++()
{
int maxday = getday(_year, _month);
if (_day + 1 <= maxday)
_day++;
else
{
_day = 1;
if (_month + 1 <= 12)
_month++;
else
{
_month = 1;
_year++;
}
}
return *this;
}
Date& Date::operator--()
{
if (_day - 1 >= 1)
_day--;
else
{
if (_month - 1 >= 1)
{
_month--;
_day = getday(_year, _month);
}
else
{
_year--;
_month = 12;
_day = 31;
}
}
return *this;
}
//使用了传引用
Date& Date::operator+=(int day)
{
if (day < 0)
return *this -= (-day);
_day += day;
int maxday = getday(_year, _month);
while (_day > maxday)
{
_day -= maxday;
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
maxday = getday(_year, _month);
}
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;
}
int maxday = getday(_year, _month);
_day += maxday;
}
return *this;
}
//使用传值(拷贝的方式)
Date Date::operator+(int day) const
{
Date temp = *this;
temp += day;
return temp;
}
Date Date::operator-(int day) const
{
Date temp = *this;
temp -= day;
return temp;
}
//日期-日期
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
if (max < min)
{
Date temp = max;
max = min;
min = temp;
}
int cnt = 0;
//暴力
while (min < max)
{
cnt++;
min+=1;
}
return cnt;
}
//不加引用是值传递,会重新拷贝一份,此时的out不再是使用者传递的对象->导致编译器无法正确识别和重载该运算符,
// 因为返回类型和参数类型都没有形成一个唯一的标识。
//加引用作用:避免对象的复制并且能够保证参数的唯一性
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year<<' '<< d._month <<' ' << d._day << endl;
return out;//返回out支持链式操作
}
istream& operator >> (istream& in, Date& d)
{
while (1)
{
cout << "请输入日期:" << endl;
in >> d._year >> d._month >> d._day;
if (!d.checkdate())
{
d.Print();
cout << "输入不合法,请重新输入!" << endl;
}
else break;
}
return in;//支持链式读取
}
test.cpp
#include "Date.h"
void test1()
{
Date d1(2024, 10, 12);
Date d2(1900, 6, 9);
d1.Print();
d2.Print();
}
void test2()
{
Date d1(2024, 10, 12);
Date d3 = d1 - 10;
d3.Print();
d3 += 100;
d3.Print();
}
void test3()
{
Date d1(2024, 10, 12);
Date d2(1900, 6, 9);
bool result1 = (d1 == d2);
cout << "d1和d2是否相等:" << result1 << endl;
bool result2 = (d1 >= d2);
cout << "d1是否大于等于d2:" << result2 << endl;
bool result3 = (d1 <= d2);
cout << "d1是否小于等于d2:" << result3 << endl;
}
void test4()
{
Date d1(2024, 10, 12);
Date d2(1900, 6, 9);
int count = d1 - d2;
cout << "d1和d2差" << count << "天" << endl;
}
void test5()
{
Date d;
cin >> d;
cout <<"当前月份的天数:" << d.getday(d) << endl;
cout << "2023年7月的天数:" << d.getday(2023, 7) << endl;
}
int main()
{
test1();
test2();
test3();
test4();
test5();
return 0;
}
六、const成员
const修饰类成员函数时,实际上修饰的是类成员函数的this指针。表明在该成员函数中不能对类的任何成员进行修改。
#include<iostream>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
void Print() const
{
cout << "const:";
cout << _year << " " << _month << " " << _day << endl;
}
};
int main()
{
//常量对象
const Date d1(2023, 6, 9);
//非常量对象
Date d2(2024, 10, 15);
d1.Print();
d2.Print();
//常量对象调用常量成员函数(d1调用Print() const函数) (权限平移,正确)
//常量对象调用非常量成员函数(d1调用Print()函数) (权限放大,错误)
//非常量对象调用常量成员函数 (d2调用Print() const函数) (权限缩小,正确)
//非常量对象调用非常量成员函数 (d2调用Print()函数) (权限平移,正确)
return 0;
}
总结:
(1)权限只能平移或缩小,不能放大
(2)const成员函数内部和非const成员函数内部均可以调用其他常量成员函数或非常量成员函数
(3)成员函数内部不需要修改成员变量时,都可以加上const,方便常量对象调用。
(4)const成员函数中不能对类的任何成员进行修改。
七、取地址操作符重载
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
取地址操作符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
结语
本篇博客我们学习了类的六个默认成员函数,重点介绍了构造函数,析构函数,拷贝构造函数,赋值运算符重载。
如果有什么建议或补充,亦或是错误,大家可以在评论区提出。
END