目录
5.2.2 赋值运算符只能重载成类的成员函数不能重载成全局函数
1. 类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中并不是什么都不写,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
- 初始化和清理
- 构造函数主要完成初始化工作
- 析构函数主要完成清理工作(不是销毁)
- 拷贝复制
- 拷贝构造是使用同类对象初始化创建对象
- 赋值重载主要是把一个对象赋值给另一个对象
- 取地址重载
- 普通对象和const对象取地址,这两个很少会自己实现
2. 构造函数
2.1 概念
#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.Init(2024,11,18);
d1.Print();
Date d2;
d2.Init(2024,11,19);
d2.Print();
return 0;
}
对于class Date 如果每次创建对象时,都调用init方法初始化信息,太过麻烦,所以C++中就可以在对象创建时就将信息设置进去。
构造函数是一个特殊的成员函数,其名字与类名相同,创建类类型对象时由编译器自动调用,在对象整个生命周期中只调用一次。
2.2 特性
构造函数不是普通的函数,构造函数主要的任务不是开空间创建对象,而是初始化对象。
- 无返回值(也不需要写void之类的)
- 对象实例化时编译器自动调用对应的构造函数
- 函数名与类名相同
- 构造函数可以重载
1 class Date
2 {
3 public:
4 Date()
5 {}
6
7 Date(int year, int month, int day)
8 {
9 _year = year;
10 _month = month;
11 _day = day;
12 }
13 private:
14 int _year;
15 int _month;
16 int _day;
17 };
18
19 void test()
20 {
21 Date d1;
22 Date d2(2024, 11,11);
23 Date d3();
24 }
21行在调用无参构造函数,22行在调用带参的构造函数,可见构造函数的调用也与普通函数不同,对象加参数列表会自动调用。
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就变成了函数声明,就像23行一样,变成了声明d3函数。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
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;
};
int main()
{
Date d1;
return 0;
}
将构造函数注释掉后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数,结果是随机值。
如果将构造函数放开,代码编译失败,一旦显式定义任何构造函数,编译器将不再生成无参构造函数,会报错,找不到无参的构造(我们定义的是有参数的构造函数)
- 不实现构造函数的情况下,编译器会生成默认的构造函数,但它自动生成的默认构造函数看起来又没什么用,d1调用了默认构造,但年月日数据是随机值
解答:C++把类型分成了内置类型与自定义类型。内置类型就是语言提供的类型,比如int、char...,自定义类型就是我们自己定义的类型,编译器生成的默认构造函数会对自定义类型成员调用它的默认类成员函数。
26 #include <iostream>
27 using namespace std;
28
29 class Time
30 {
31 public:
32 Time()
33 {
34 cout << "Time()" << endl;
35 _hour = 0;
36 _minute = 0;
37 _second = 0;
38 }
39
40 private:
41 int _hour;
42 int _minute;
43 int _second;
44 };
45
46 class Date
47 {
48 private:
49 int _year;
50 int _month;
51 int _day;
52
53 Time _t;
54 };
55
56 int main()
57 {
58 Date d2;
59 return 0;
60 }
代码中53行即为自定义类型,上面的49、50、51为基本类型。
在创建d2对象时,会调用默认构造,对于Time类的自定义类型,会调用它的构造函数,初始化为0。但有些编译器不会对内置类型进行处理,甚至是VS2019,加了自定义类型会对内置类型进行处理,不加自定义类型不会对内置类型处理。但是为了避免问题,我们要当它不会进行处理,所以面对内置类型,我们都要自己写构造函数,而对于全是自定义成员的类,可以考虑让编译器自己生成。
注意:在C++11中针对内置类型成员不初始化的缺陷,又做了更新,内置类型成员变量在声明时可以给默认值。
26 #include <iostream>
27 using namespace std;
28
29 class Time
30 {
31 public:
32 Time()
33 {
34 cout << "Time()" << endl;
35 _hour = 0;
36 _minute = 0;
37 _second = 0;
38 }
39
40 private:
41 int _hour;
42 int _minute;
43 int _second;
44 };
45
46 class Date
47 {
48 private:
49 int _year = 2024;
50 int _month = 11;
51 int _day = 20;
52
53 Time _t;
54 };
55
56 int main()
57 {
58 Date d2;
59 return 0;
60 }
49、50、51不是初始化,而是声明,没有开辟空间,后面的值是默认的缺省值,是给默认的构造函数用的。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。无参构造函数、全缺省构造函数、我们没写编译器自己生成的构造函数,都可以叫做默认构造函数。
62 class Date
63 {
64 public:
65 Date()
66 {
67 _year = 2024;
68 _month = 11;
69 _day = 20;
70 }
71
72 Date(int year = 2024, int month = 11, int day = 20)
73 {
74 _year = year;
75 _month = month;
76 _day = day;
77 }
78
79 private:
80 int _year;
81 int _month;
82 int _day;
83 };
84
85 void test()
86 {
87 Date d1;
88 }
代码中有两个构造函数,其构成函数重载,但是test中是无参调用,无参调用时存在歧义,无参的和全缺省的都可以调用,该用哪个呢?编译时不会通过,所以不能同时存在,保留的话建议留全缺省的,该种方式可以有多种调用方式,比如year和month用缺省,day自己初始化。
结论:
1.一般情况下,构造函数都要我们自己写
2.内置类型成员都有缺省值,且初始化符合我们的要求
3.全是自定义类型的,且这些类型都定义了默认构造函数,可以不写构造函数
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 = 2024;
int _month = 12;
int _day = 1;
Time _t;
};
int main()
{
Date d;
return 0;
}
程序结束后输出~Time(),在main中并没有直接创建Time类对象,为什么会调用Time类的析构函数?
因为main中创建了对象d,d包含4个成员变量,其中前三个是内置类型,销毁时不需要资源清理,而 _t是Time类对象,也就是自定义类型对象,在d销毁时,要将d中包含的_t对象销毁,所以要调用Time类的析构函数。
但是main不能直接调用Time类的析构函数,main中主要是释放date类的对象,所以会调用Date的析构函数,Date没有显式给析构,编译器生成一个默认析构函数,其目的是调用Time的析构函数,当对象d销毁时,要保证其内部每个自定义对象都可以正确销毁。
6. 如果类中没有申请资源(malloc)时,比如Date类,可以不写析构函数,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄露,比如stack类;需要释放资源的成员都是自定义类型,也不需要写析构,因为会调用成员自己的析构函数。
4. 拷贝构造函数
4.1 概念
如果我们想要创建一个和已存在对象一模一样的新对象,就需要用到拷贝构造函数,其只有单个形参,该形参是对本类类型对象的引用(一般会用const修饰,权限可以缩小,不能放大),在已存在的类类型对象创建新对象时由编译器自动调用。
4.2 特征
同样的,拷贝构造函数也是特殊的成员函数。
1. 拷贝构造是构造函数的一个重载形式(参数不同)
2. 拷贝构造的参数只有一个,使用传值方式编译器会直接报错,因为引发了无穷递归调用。
class Date
{
public:
Date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
其中Date(const Date& d)是正确的写法,如果写Date(Date d)编译器会报错,会引发无穷递归,d是d1的别名,递归过程:当代码运行到Date d2(d1),想要调用拷贝构造,但是调用函数要先传参,由于这里是传值调用,传值就会引发对象的拷贝,调用拷贝构造(这个拷贝构造又需要传参),但是我们之前就在调用拷贝构造,引发无穷递归。
我们可以做一个实验,若写一个普通的函数func(d1),不会跳转到函数声明和定义部分,而是先传参,传参就会跳转到Date(Date& d),走完回到func(d1),再走才会进入函数声明和定义内。
C++规定:自定义类型调用拷贝构造完成拷贝,内置类型直接拷贝。
3.若未显式定义,编译器会生成默认拷贝构造函数,默认拷贝构造是按内存存储字节序完成拷贝,这种拷贝是浅拷贝
在编译器生成的默认构造拷贝中,内置类型是按照浅拷贝进行的,而自定义类型是调用其拷贝构造函数完成拷贝的。所以Date和MyQueue不需要,stack要自己写。
4. 编译器生成的默认拷贝构造已经可以完成字节序的拷贝了,像日期类这样的类是没有必要的,那么涉及到内存申请的类呢?
typedef int Datetype;
class stack
{
public:
stack(size_t capcity = 10)
{
_arr = (Datetype*)malloc(capcity*sizeof(Datetype));
if (nullptr == _arr)
{
perror("malloc fail");
return;
}
_size = 0;
_capcity = capcity;
}
void push(const Datetype& x)
{
_arr[_size] = x;
_size++;
}
~stack()
{
if(_arr)
{
free(_arr);
_arr = nullptr;
_capcity = 0;
_size = 0;
}
}
private:
Datetype *_arr;
size_t _size;
size_t _capcity;
};
int main()
{
stack s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
stack s2(s1);
return 0;
}
假设这个stack不是库中的,而是我们自己实现的,运行时程序会崩溃,为什么呢?
当我们进行拷贝构造时,使用了编译器默认生成的,s1对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间,s2对象使用s1拷贝构造,按照值拷贝,将s1中内容原封不动的拷贝到s2中,因此s1和s2指向了同一块内存空间。
目前都没问题,当程序退出时,注意,s2和s1都要销毁,s2先销毁(后定义的先析构),调用析构函数,将空间释放了,当s1销毁时,又调用析构函数,想要将空间再释放一次,但s2与s1指向的是同一块空间,同一块空间释放多次,程序崩溃。
所以当申请资源时,就会涉及到内存地址,不写拷贝构造就会浅拷贝,拷贝空间地址,导致析构两次。一旦涉及到资源申请,拷贝构造函数是一定要写的。
这里还有另一个原因,比如我们像s1插入新的值,我们是不希望s2也插入这个值的,这不符合我们拷贝构造的初衷,如果浅拷贝,修改s1会影响s2(因为指向同一块空间),我们希望他们是独立的。
5.为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量用引用。
但注意,如果一个参数出了作用域会销毁,那么就不能用引用返回。
5. 赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能连接其他符号来创建新的操作符:operator@
- 用于内置类型的运算符,因为编译器知道要怎么比较,其含义是不能变的
- 作为类成员函数重载时,其形参看起来比操作数数目少1,是因为成员函数的第一个参数是隐藏的this指针
- .* :: sizeof ?: . 以上5个运算符不能重载,这个要重点记,在笔试选择题中出现
如果我们想要比较两个日期的大小,该怎么写代码呢?
日期类是自定义类型,编译器并不知道怎么比较大小,所以需要我们重载,内置类型是可以直接比较的。
#include <iostream>
using namespace std;
class date
{
public:
date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
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;
}
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;
}
return false;
}
int main()
{
date d1(2025,1,1);
date d2(2025,1,15);
cout << (d1 < d2) << endl;
cout << (operator<(d1,d2)) << endl; 这行与上一行是等效的,写成上面的可读性比较强,会自动转化成这一行。
cout << (d1 > d2) << endl;
cout << (operator>(d1,d2)) << endl;
return 0;
}
这个代码会报错,是因为在类外的函数访问了类中的私有成员,如果我们把私有成员改为公有的,是可以实现的,可实际中为了隐藏类的内部实现细节,只对外提供有限的接口,这种封装方式能够保护对象的状态不被外部代码直接修改,我们往往把成员变量设置为私有的,该怎么做呢:将bool operator写进类中作为成员函数,但还是会报错,这是为什么呢?我们上面提到过“作为类成员函数重载时,其形参看起来比操作数数目少1,是因为成员函数的第一个参数是隐藏的this指针”,我们的operator此时已经有两个参数了,如果再算上隐藏的this,就有3个参数了
那我们就应该做如下修改,将参数改为1个
#include <iostream>
using namespace std;
class date
{
public:
date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const date& x1) const
{
if (_year < x1._year)
{
return true;
}
else if (_year == x1._year && _month < x1._month)
{
return true;
}
else if (_year == x1._year && _month == x1._month && _day < x1._day)
{
return true;
}
return false;
}
bool operator>(const date& x1) const
{
if (_year > x1._year)
{
return true;
}
else if (_year == x1._year && _month > x1._month)
{
return true;
}
else if (_year == x1._year && _month == x1._month && _day > x1._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2025,1,1);
date d2(2025,1,15);
cout << (d1 < d2) << endl;
// cout << (operator<(d1,d2)) << endl;
cout << (d1 > d2) << endl;
// cout << (operator>(d1,d2)) << endl;
return 0;
}
d1 < d2 会自动转化成 d1.operator<(d2),这时operator看上去是一个参数,其实是两个,操作数 d1 是调用对象,因此它的地址会通过隐式的 this 指针传递给operator<成员函数。第二个操作数 d2 则是显式地作为参数传递。
最后请注意:d1-d2是有意义的,代表相差几天,但是d1+d2是没有意义的,所以在日常使用中,是否要重载运算符,取决于运算符对类是否有意义。
5.2 赋值运算符重载
5.2.1 赋值运算符重载格式
- 参数类型:const T& 传引用可以提高传参的效率
- 返回值类型:T& 引用返回可以提高返回的效率,有返回值的目的是为了连续赋值
下面的赋值重载是最简单的版本,但是是有问题的。
class date
{
public:
date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
date (const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void operator= (const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
首先,当我们在连续赋值时,比如 d3 = d2 = d1 , d2 = d1 部分实际上是 d2.operator=(d1),它的返回值是void,这是不合理的,返回的应该是d2,this是d2的地址,所以在最后要加一个return *this。因为赋值操作不影响d2的生命周期,d2的生命周期从创建这个对象就开始了,在main函数返回时结束,所以可以return *this ,this指针在这个函数结束后销毁,但是对象不会。
class date
{
public:
date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
date (const date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
date& operator= (const date& d)
{
if (this != &d) 避免d1 = d1
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1(2025,1,1);
date d2(2025,1,15);
d2 = d1;
return 0;
}
赋值语句 d2 = d1;
不会重新创建 d2
,只会调用赋值运算符 operator=
来更新 d2
的数据成员。
其次, date& operator= (const date& d) 为什么要传引用返回,传值返回 date 是可以,但是代价比较大了,会调用一次拷贝构造:return *this;
会创建一个新的临时对象来存储 *this
的值,这一步会调用一次拷贝构造函数。如果我们是连续赋值,就会触发多次拷贝构造,大大降低效率,所以用引用返回。
注意:
传递自定义类型的指针不会调用拷贝构造函数,因为指针只是地址的拷贝,而不是对象本身的拷贝。
拷贝构造函数只在拷贝对象本身时被调用,例如:
- 按值传递对象。
- 按值返回对象。
- 显式初始化新对象时。
5.2.2 赋值运算符只能重载成类的成员函数不能重载成全局函数
赋值运算符重载是特殊的,是默认成员函数,如果类中没有,会自动生成一个,如果重载成全局函数,会和类中默认生成的产生冲突,故赋值运算符重载只能是类的成员函数。
如果是其他运算符,他们不是默认成员函数,不写不会自己生成,所以可以重载成全局函数,但并不是所有的运算符都适合重载为全局函数,但确实有一些运算符更适合这样设计,比如双目运算符,但是由于私有成员需要借用到友元才能访问,这会破坏封装性。
举个例子:
- 需要访问私有成员:运算符如
+
是一个非成员函数时,如果需要访问类的私有数据,就必须将它声明为友元。 - 对称性:
+
运算符通常是对称的,表示两个对象之间的相加操作。如果定义为成员函数,则左侧操作数必须是当前类的对象,而右侧可以是任意类型(包括基本类型)。但使用友元函数,可以更灵活地支持左右操作数的互换(如obj1 + obj2
或obj2 + obj1
)。
class MyClass
{
private:
int value;
public:
MyClass(int v) : value(v) {}
MyClass operator+(const MyClass& rhs) const
{
return MyClass(value + rhs.value);
}
};
int main()
{
MyClass a(10), b(20);
MyClass c = a + b; // 正常工作
// MyClass d = 10 + a; // 错误,左操作数不是 MyClass 类型
return 0;
}
如上所示,左操作数必须是 MyClass
类型,因为 +
是 MyClass
的成员函数。如果需要支持 10 + a
,必须使用友元函数。
class MyClass
{
private:
int value;
public:
MyClass(int v) : value(v) {}
friend MyClass operator+(const MyClass& lhs, const MyClass& rhs);
};
// 全局友元函数
MyClass operator+(const MyClass& lhs, const MyClass& rhs)
{
return MyClass(lhs.value + rhs.value);
}
int main()
{
MyClass a(10), b(20);
MyClass c = a + b; // 正常
MyClass d = 10 + a; // 如果支持混合类型,可以额外重载
return 0;
}
当运算符重载确实需要访问私有数据,并且定义为非成员函数可以实现更灵活的操作时,使用友元是合理的。
5.2.3 默认赋值运算符重载是以值方式拷贝
当一个date类中既有自定义类型,也有内置类型成员时,若d1 = d2 内置类型成员是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class date
{
private:
int _year = 2024;
int _month = 12;
int _day = 1;
Time _t;
};
int main()
{
date d1;
date d2;
d1 = d2;
return 0;
}
像日期类这种编译器默认生成的赋值运算符重载函数已经可以完成字节序的值拷贝了,没必要自己实现,但是如果一个类需要资源申请,就需要自己实现了:
typedef int datetype;
class stack
{
public:
stack(size_t capacity = 10)
{
_arr = (datetype*)malloc(capacity * sizeof(datetype));
if (nullptr == _arr)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
void push(const datetype& data)
{
_arr[_size] = data;
_size++;
}
~stack()
{
if(_arr)
{
free(_arr);
_arr = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
datetype *_arr;
size_t _size;
size_t _capacity;
};
int main()
{
stack s1;
s1.push(1);
s1.push(2);
s1.push(3);
s1.push(4);
stack s2;
s2 = s1;
return 0;
}
这里s2 = s1 使用的是默认生成的赋值运算符重载,值拷贝方式,我们来分析一下这个代码:
1. s1对象创建后,开辟了10个元素的空间,插入了4个数据
2. s2对象创建后,开辟了10个元素的空间,没有插入数据
3. s2 = s1,当s1给s2赋值时,编译器会将s1中内容原封不动的拷贝到s2中,此时s2中的_arr指向的和s1中_arr指向的是同一块空间,s2原来的空间就找不到了,会导致内存泄露;s1和s2共享同一块空间,最后销毁时,这块空间会被析构两次,导致程序崩溃。
5.3 前置++和后置++重载
class date
{
public:
date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//this指向的对象函数结束后不会销毁,用引用方式返回
date& operator++ ()
{
_day += 1;
return *this;
}
date operator++ (int)
{
date temp(*this);
_day += 1;
return temp;
}
void print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d;
date d1(2024,12,2);
cout << "d: ";
d.print();
cout << "d1: ";
d1.print();
d = d1++;
cout << "\nAfter d = d1++:" << endl;
cout << "d: ";
d.print();
cout << "d1: ";
d1.print();
d = ++d1;
cout << "\nAfter d = ++d1:" << endl;
cout << "d: ";
d.print();
cout << "d1: ";
d1.print();
return 0;
}
后置++重载时多一个int类型的参数,这个参数不是为了接受具体的值,是为了占位,与前置++构成重载。后置++是先使用,所以用temp保存++前的值,然后给this+1,而temp是临时对象,只能以值的方式返回,若用引用返回,函数结束时,temp会销毁,引用不到了。
6. 日期类的实现
class date
{
public:
static int GetMonthDay(int year, int month)
{
static int days[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
int day = days[month];
if(month==2
&&((year % 4 == 0 && year % 100 != 0)||(year % 400 == 0)))
{
day += 1;
}
return day;
}
explicit date(int year = 2024, int month = 12, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
date(const date& d)
{
_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;
}
date& operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_year,_month);
++_month;
if(_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
date operator+(int day)
{
date tmp = *this;
tmp += day;
return tmp;
}
date& operator-=(int day)
{
_day -= day;
while(_day <= 0)
{
--_month;
if(_month == 0)
{
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
date operator-(int day)
{
date tmp = *this;
tmp -= day;
return tmp;
}
date& operator++()
{
*this += 1;//会自动使用 this 指向的当前对象的 _day 成员变量。你无需显式传递 _day,因为成员函数默认操作当前对象的成员变量
return *this;
}
date operator++(int)
{
date tmp = *this;
*this += 1;
return tmp;
}
date& operator--()
{
*this += 1;
return *this;
}
date operator--(int)
{
date tmp = *this;
*this -= 1;
return tmp;
}
bool operator<(const date& d) const
{
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;
}
bool operator>(const date& d) const
{
return !(*this <= d);
}
bool operator<=(const date& d) const
{
return *this < d || *this == d;
}
bool operator>=(const date& d) const
{
return *this > d || *this == d;
}
bool operator==(const date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool operator!=(const date& d) const
{
return !(*this == d);
}
int 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;
}
void print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
date d1;
date d2(2024,12,12);
d1.print();
cout << d2 - d1 << endl;
cout << d1 - d2 << endl;
// d1++;
// d1.print();
//
// // d1+1;
// // d1.print();
// // (d1+1).print();
// //
// // d1+=1;
// // d1.print();
return 0;
}
我们来看一些容易忽视的地方:
1. 一开始的GetMonthDay函数,前面为什么要加static:static
关键字将 GetMonthDay
定义为类的静态成员函数。这意味着:
- 可以直接通过类名调用,而不需要实例化对象。普通的成语函数需要有对象,例如d1.print()
- 函数不依赖任何类实例的状态。它只处理输入参数
year
和month
,没有访问类的成员变量(_year
,_month
,_day
)。因此,它是一个适合做静态函数的逻辑工具。
关于静态成员和静态成员函数,下一章会详细介绍。
2. GetMonthDay函数下的days数组,为什么要加static:static修饰符使得数组 days
的内存分配是 静态分配 的,且生命周期贯穿整个程序运行。如果没有 static
,每次调用函数时,数组 days
都会在栈上分配内存并初始化,函数退出后内存被销毁。这带来了以下好处:
- 初始化只发生一次:在函数第一次被调用时初始化,以后调用不会重复分配和初始化内存。
- 性能优化:避免在每次调用函数时都重新分配和初始化数组,提高性能。
- 共享性:
days
数组在函数的每次调用中保持一致,不会因为函数结束而销毁。
3. 构造函数前为什么要加explicit:为了防止 隐式类型转换,从而避免不必要的错误或意外行为。如果不加 explicit
,这个构造函数会被用作隐式类型转换构造函数,即在需要一个 date
类型的地方,编译器会尝试使用这个构造函数将其他类型(如整数)转换为 date
对象。
date d = 20231212; 编译器尝试用 date(int year, int month, int day) 构造对象
这会让代码编译通过,但显然 20231212
不是一个合理的日期对象输入,可能引发逻辑错误。
因为我们的构造函数是全缺省的,这个构造函数“有多个参数且默认值可以缩减为单一参数的调用”,它的意思是,尽管构造函数定义了多个参数,但因为其中的一些参数有默认值,所以可以在调用时只传递一个参数。这样的情况下,构造函数就变得类似于一个单参数的构造函数,而这会使得意外的隐式类型转换更加容易发生。
通过加上 explicit
,可以禁止这种隐式转换:
现在,以下代码会报编译错误:
date d = 20231212; // 错误:不能隐式转换 int 到 date
此时我们还是可以通过正常的显示调用构造日期对象,就像d2一样。
7. const成员
将const修饰的成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明该函数不会修改类的任何成员变量。
我们可以看到在上面的日期类实现过程中:
void print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
print()后面就加了const,那我们为什么要加这个const呢?
如果我们有一个const对象,需要在对象上调用一个成员函数,那么这个函数必须是 const
成员函数,因为const对象不可以修改类的状态,如果它能调用一个可以修改成员的函数是不合理的,编译器会认为没加const的成员函数就是会修改类的成员变量,const对象去调用就会报错。
所以非 const
对象(有修改的权限,但是可以不修改)可以调用所有的成员函数(包括 const
和非 const
),而 const
对象只能调用 const
成员函数。
在 const
成员函数中,只能调用其他的 const
成员函数,不能调用非 const
的成员函数,因为非const
成员函数可能会修改对象的状态,这违反了const
的承诺。反之成立:非const
成员函数可以修改类的状态,因此它对类的所有成员(包括const
成员函数)具有访问权限。
只要一个成员函数的职责是访问类的成员变量而不是修改它们,那么应该将它声明为 const
,以增强代码的安全性和可读性。
8. 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成。