其他类和对象文章:
类和对象(一)
【C++】类和对象(二)
六个默认成员函数
如果我们写一个空类,里面并不是什么都没有,而是自动生成六个默认成员函数。
class A{};
用户没有显式实现,而是由编译器隐式生成的函数就是默认成员函数
- 构造函数:不是构造对象,而是对对象的数据进行初始化
- 析构函数:不是销毁对象,而是清理对象申请的资源
- 拷贝构造:用一个已经存在的对象来初始化创建一个对象
- 赋值重载:把一个已存在对象赋值给另一个已存在对象
5 & 6. 取地址重载:普通对象和 const 对象取地址重载,这两个很少自己实现
构造函数
当我们创建一个对象时,会自动调用构造函数,初始化对象中的数据。这样就不需要我们手动初始化了。构造函数是一个特殊的成员函数,以下是它的一些特性:
- 函数名与类名相同
- 无返回值
class Date
{
public:
// 构造函数
Date(){}
private:
int _year;
int _month;
int _day;
};
Date d1; // 实例化时,自动调用构造函数
-
实例化对象自动调用
-
可以重载,以满足不同的初始化需求
class Date
{
public:
Date(){}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
Date d1;
Date d2(2024, 1, 1);
注意,当创建对象不需要传数据时,就不需要加括号了
Date d1;
Date d1(); // 这种形式是不行的
因为加了括号,却没有参数,编译器就会认为这是函数声明
- 用户没有显示实现,编译器自动生成默认构造函数;如果用户自己实现了,编译器不再自动生成
class Date
{
public:
// 不写就会自动生成一个默认构造
private:
int _year;
int _month;
int _day;
};
Date d1;
- 默认生成的构造函数,对内置类型不做处理,对自定义类型调用它的构造函数
通过上图我们可以看到,编译器自动生成的构造函数,并不会对数据进行处理,那它有什么用呢?
C++ 的数据类型分为内置类型和自定义类型,内置类型是语言提供的类型,如 int/char/long 等,自定义类型是我们使用 class/struct/union 定义的类型
如果一个类中嵌套了另一个类,如下:
class A
{
public:
A()
{
cout << "A()" << endl;
}
};
class Date
{
public:
// 默认构造
private:
int _year;
int _month;
int _day;
A _a;
};
Date d1;
在我们实例化一个 Date 对象时,默认生成的构造函数不会对 _year/_month/_day 等内置类型做处理,但是会调用自定义类型 A 的构造函数来初始化 A
在编译器默认生成构造函数时,如果要初始化内置数据,可以在类的数据声明阶段,给数据设置缺省值
class Date
{
public:
// 默认构造
private:
// 这是声明,不是定义
int _year = 1;
int _month = 1;
int _day = 1;
A _a;
};
- 默认构造函数不只是编译器自动生成的,还有无参构造函数、全缺省构造函数。简单来说,不用传参的构造函数就是默认构造函数
先来看一下代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
Date d1(2024, 1, 1);
此时运行代码,就会出现问题:
这里的报错是:没有合适的默认构造函数可用。虽然我们写了一个构造函数,但它不是默认构造函数,而一个类必须要有一个默认构造函数。只要我们写了构造函数,无论算不算默认构造,编译器都不会再自动生成默认构造函数了
默认构造函数有以下三种:
- 用户没写,编译器自动生成的默认构造函数
- 用户写了,无参构造函数
- 用户写了,全缺省构造函数
也就是实例化对象时不需要传参,这种构造就是默认构造函数。以上三种默认构造互斥,也就是只能有一个默认构造。一般都推荐使用全缺省
互斥:用户写了,编译器不再自动生成;而用户写的无参和全缺省也是互斥的,如果调用时没有参数,编译器不知道要调用哪个
析构函数
当对象的生命周期结束时,析构函数自动调用,释放对象申请的资源,如 malloc、new 出来的空间。析构函数也是一个特殊的成员函数,特性如下:
- 函数名是在类名前加一个
~
,例如~Date
- 无参数,无返回值
- 一个类只能有一个析构函数,不可重载
- 用户没有显式定义析构函数,编译器就会自动生成一个默认析构函数
- 默认析构函数对内置类型不做处理,对于自定义类型则去调用它的析构函数
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
A _a;
};
Date d1;
对于日期类这种,没有申请资源,数据都放在栈上,等对象的生命周期结束,数据自然会被销毁,不用自己写析构函数,使用编译器自动生成的析构函数即可
但是如果有资源申请,例如 Stack 类,默认析构函数只会销毁指向申请空间的指针,并不会清理额外申请的空间,会造成资源泄露问题
所以如果类中有申请空间,就需要我们手动显示实现析构函数,清理空间
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 TestStack()
{
Stack s1;
s1.Push(1);
s1.Push(2);
}
拷贝构造
拷贝构造是一个特殊的构造函数,可以创建一个与已存在对象相同的新对象。在我们用已存在对象创建新对象时,会自动调用
拷贝构造是一个特殊的构造函数,也就是说在我们显式实现一个拷贝构造的同时,也必须实现一个默认构造函数。以下是拷贝构造的一些特性:
- 拷贝构造是构造函数的一个重载
- 拷贝构造的参数是一个类类型对象的引用,如果不是引用则会引发无穷递归问题
这种是正确的写法,参数是引用
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
Date d1(2024, 1, 1);
Date d2(d1);
d1.Print();
d2.Print();
而如果拷贝构造的参数不是引用,就会引发无穷递归。因为传值传的是形参,也就是实参的拷贝。既然是拷贝,就会触发新的拷贝构造,这样就会无限递归下去
// 拷贝构造
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法
{
_year = d._year;
_month = d._month;
_day = d._day;
}
- 若用户未显式定义拷贝构造,编译器就会自动生成一个默认拷贝构造函数。默认拷贝构造函数会按照字节序完成拷贝,这种拷贝叫做浅拷贝,也叫值拷贝
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 = 1;
int _month = 1;
int _day = 1;
// A _a;
};
Date d1(2024, 1, 1);
Date d2(d1);
d1.Print();
d2.Print();
- 默认构造函数会对内置类型做浅拷贝,对自定义类型则调用它的拷贝构造
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
};
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 = 1;
int _month = 1;
int _day = 1;
A _a;
};
Date d1(2024, 1, 1);
Date d2(d1);
d1.Print();
d2.Print();
- 和析构函数一样,日期类这种不用申请资源的类,默认拷贝构造就够用了。但是一旦涉及到资源申请,就必须要我们自己实现拷贝构造了
还是用之前的 Stack 类测试一下,看看用自动生成的默认拷贝构造会怎样
void TestStack()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
}
程序崩溃了:
这是因为默认拷贝构造使用的是浅拷贝,s2 只是把 s1 中的 _array 指针拷贝了,并没有拷贝空间
到了程序结束时,s1 和 s2 都是要销毁的。s2 后创建,所以先销毁,调用析构函数,将申请的空间资源释放。然后 s1 销毁,调用析构函数,释放同样的空间,对一块空间释放两次是非法操作,所以会造成程序崩溃
所以当类中有资源申请时,就要手动实现一个拷贝构造,对申请的空间中的数据也进行拷贝,就是深拷贝
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(sizeof(DataType) * s._capacity);
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, s._size * sizeof(DataType));
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
赋值运算符重载
运算符重载
内置类型可以使用运算符,例如+、-、++
等,那么自定义类型可不可以使用这些运算符呢?
显然是不可以的,因为编译器并不认识这些自定义类型,这时就需要我们进行运算符重载了
运算符重载方法:返回值 operator操作符(参数列表),例如
bool operator==(const Date& d1, const Date& d2)
注意:
- 只能重载已经存在的操作符,不能自己创造新的操作符,例如 operator@
- 重载操作符不能改变操作符原本的含义,例如 operator++ 只能实现 ++ 功能
- 不能重载的 5 个运算符:
.* . :: sizeof ?:
- 重载操作符必须有一个类类型参数
- 重载为成员函数时,形参的数目是少一个的,因为有一个参数是隐函的 this 指针,例如:
bool operator==(const Date& d); // 第一个参数是 this 指针,*this == d
下面我们尝试为 Date 重载一个全局函数 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 = 1;
int _month = 1;
int _day = 1;
};
bool operator== (const Date & d1, const Date & d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
问题来了,全局的 operator== 无法访问成员变量,有三个措施:
- 将成员变量设为公有,为了让不破坏类的封装性,一般不会这么做
- 将 operator 声明为 Date 的友元函数,如下
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;
}
// 友元函数
friend bool operator== (const Date & d1, const Date & d2);
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
bool operator== (const Date & d1, const Date & d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
- 将 operator== 重载为Date的成员函数
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator== (const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
重载为成员函数时,虽然看起来形参少一个,但是实际 this 指针是第一个参数
赋值重载
赋值重载也是默认成员函数之一,可以让我们将一个已存在的对象赋值给另一个已存在的对象
- 参数:类类型的引用,这样传参可以避免传值拷贝,提升效率
- 返回值:类类型的引用,不仅可以提高效率,可以支持连续赋值,例如:
int i = 1, j = 2;
i = j = 3; // 连续赋值
尝试重载 Date 类型的 =
Date& operator=(const Date& d)
{
// 检查自己给自己赋值
if (*this == d)
return *this;
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
Date d1(2024, 1, 1);
Date d2(2025, 2, 2);
Date d3(2026, 3, 3);
d1 = d2 = d3;
d1.Print();
d2.Print();
d3.Print();
此外,赋值重载同样有以下特性:
- 如果用户没有显式实现赋值重载,编译器就会自动实现一个默认的
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator== (const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
-
同拷贝构造一样,默认的赋值重载只会进行浅拷贝,如果类中存在资源申请,就必须手动实现一个赋值重载
-
赋值重载不可以重载为全局函数,只能是成员函数。因为如果类中不写,就会自动生成一个,此时就会与全局的赋值重载发生冲突
前置++ 和 后置++ 重载
前置++:先加1,后使用。因此可以这样实现:
Date& operator++()
{
_day++;
return *this;
}
返回引用可以使效率更高
后置++:先使用,后加1。这就要将加1之前的值临时拷贝一份,然后加1,再返回加1之前的值。这样就不可以返回引用了
前置++的重载已经用了operator++,那么后置++的重载该如何呢?那只能重载了
为了构成重载,C++规定,后置++的重载要这样写:参数中加一个 int 类型,但是使用时不用传参
Date operator++(int)
{
Date tmp(*this);
_day++;
return tmp;
}
日期类的实现
学习了上述四个默认成员函数之后,就可以进行日期类的编写了
模块化
- test.cpp 用于测试
- Date.h 包含需要的头文件与类的定义、成员函数的声明
- Date.cpp 成员函数的定义
成员变量
我们这里的日期类不要求多么精细,只要能表达清楚最基础的年月日即可。为了后续测试,这里写一个 Print 用来打印年月日
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
构造函数
通常我们都是把构造函数写为全缺省形式,这样创建对象时传不传参都可以
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
注意函数声明定义分离时,缺省值不能同时都给,要写在声明中
析构函数,拷贝构造,赋值重载对于日期类来说只用自动生成的就够了,接下来我们来看日期类的运算符重载
运算符重载
< 和 ==
先实现 < 和 ==,然后其他的运算符 !=
>
>=
等等就可以复用了
在实现 < 时,我们可以把小于的情况都列出来,其他情况不管
- 年小,就是小于;年相等,比较月
- 月小,就是小于;月相等,比较日
- 日小,就是小于
其他情况就不管了,代码如下
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)
{
if (_day < d._day)
return true;
}
}
return false;
}
而 == 就比较好实现了,只有当两个对象的年月日严格相等时,就返回true
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
测试:
目前来看没有问题,下面就复用这两个代码,实现其他运算符重载
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)
{
if (_day < d._day)
return true;
}
}
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);
}
+= 和 +
日期加日期是没有意义的,而日期加天数x可以算出x天后的日期
我们这里先实现 += ,然后实现 + 时就可以复用了。这里先留个问题:为什么不先实现 +,再复用实现 += 呢?
首先要明确返回值:+= 支持连续赋值,所以我们要将 += 过后的数返回,返回类型可以使用引用。
再来看实现:
- 先让日期加上要加的天数x
Date& Date::operator+=(int x)
{
_day += x;
}
- 然后处理日期越界的情况,例如 2024/1/10 += 50 结果为 2024/1/60,显然是不合理的,因此需要做进位处理:_day超过当月天数,就减去当月天数, _month进一;
- 如果_month满了,就将 _month 设为1,_year++。
- 循环上述操作,直到_day小于当月天数
因为要用到当月的天数,这里就写一个成员函数,用于获取当月的天数,同时还要考虑到闰年的 2 月有 29 天。如下:
int GetMonthDays(int year, int month)
{
static int days[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 days[month];
}
然后是 operator+= 函数
Date& Date::operator+=(int x)
{
_day += x;
// 进位处理
while (_day > GetMonthDays(_year, _month))
{
_day -= GetMonthDays(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
这里就可以测试了,为了验证代码写的对不对,可以随便找一个日期计算网站验证一下计算结果
Date d1(2024, 1, 1);
d1 += 100;
d1.Print();
测试结果:
初步测试应该是没问题的,接下来直接复用 += 实现 +
+ 运算符不能改变原数据,又要返回 原数据+x 的值,所以只能拷贝一份原数据,再 += x,并返回原数据。这里返回值就不可以是引用了
Date Date::operator+(int x)
{
Date tmp(*this); // 拷贝构造
tmp += x;
return tmp;
}
测试代码
Date d1(2024, 1, 1);
Date d2;
d2 = d1 + 1000;
d2.Print();
测试结果:
到这里,就算完成了 += 和 + 的重载了。这时我们就要解决开始提出的问题了:为什么不先实现 +,然后复用实现 += 呢?
以下是先实现 +,然后复用实现 += 的代码
Date Date::operator+(int x)
{
Date tmp(*this); // 拷贝消耗
tmp._day += x;
// 进位处理
while (tmp._day > GetMonthDays(tmp._year, tmp._month))
{
tmp._day -= GetMonthDays(tmp._year, tmp._month);
tmp._month++;
if (tmp._month == 13)
{
tmp._month = 1;
tmp._year++;
}
}
return tmp;
}
Date& Date::operator+=(int x)
{
*this = *this + x; // 调用+,而+有拷贝消耗
return *this;
}
经过对比两种写法,我们可以看出来,它们的拷贝消耗是不同的:
- 先实现 +=,再复用实现 +。+=没有拷贝构造的消耗,而 + 有拷贝构造的消耗
- 先实现 +,再复用实现 +=。+=有拷贝消耗,+有拷贝消耗
所以我们选择消耗少的一种
-= 和 -
日期可以加天数,也就有日期减天数。这里我们依然是先实现 -=,再实现 -,不再解释
实现:
- 先将 _day 减去天数 x
Date& Date::operator-=(int x)
{
_day -= x;
}
- 处理越界情况,例如 2024/1/10 -= 20 结果为 2024/1/-10,这是不合理的,需要向_month借位
- _month–,如果 _month == 0,就需要向 _year 借位,将 _month 设为 12
- _day 加上当月天数
- 循环上述操作,直到 _day > 0
代码:
Date& Date::operator-=(int x)
{
_day -= x;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDays(_year, _month);
}
return *this;
}
Date Date::operator-(int x)
{
Date tmp(*this);
tmp -= x;
return tmp;
}
测试:
++ 和 –
不管是前置++、–,还是后置++、–,可以复用 += 和 -= 来实现,代码如下:
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
测试:
以下是前置–和后置–
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
测试:
日期减日期
日期加日期没有意义,但是日期减日期可以算出两个日期的天数差
直接让两个日期相减不好处理,但是我们可以这样实现:
- 区分出两个日期的大小
- 小日期循环++,直到小日期==大日期;设置一个变量 cnt,每次循环 cnt++
- 假设第一个参数大,设置标记 flag = 1;如果是第二个参数大,设置标记 flag = -1
- 最后返回 cnt*flag
这样虽然效率比较慢,但是实现很简单。代码如下:
int operator-(Date& d)
{
Date max(*this);
Date min(d);
int flag = 1;
if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int cnt = 0;
while (min != max)
{
min++;
cnt++;
}
return cnt * flag;
}
测试:
流插入<<
默认的流插入识别不出自定义类型,所以如果我们想直接打印Date对象就需要重载流插入运算符<<
因为流插入操作符支持连续操作,所以返回值要返回流的引用,参数则是流对象的引用
ostream& Date::operator<<(ostream& out)
{
out << _year << "/" << _month << "/" << _day << endl;
return out;
}
但是这样写是有问题的:
如果重载为成员函数,那么参数列表的第一个参数就是隐含的 this 指针。调用时是这样的顺序:输出的数据 << 流
ostream& Date::operator<<(Date* this, ostream& out)
而我们正常使用流插入应该是这样的: 流 << 输出的数据,第一个参数是流,第二个参数是类类型
所以流插入和流提取应该重载为全局函数,在类中声明为友元函数
class Date
{
public:
friend ostream& operator<<(ostream& out, const Date& d);
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day;
return out;
}
测试:
流提取>>
流提取也是重载为全局函数,之后声明为Date的友元函数
class Date
{
public:
friend istream& operator>>(istream& in, Date& d);
}
istream& operator>>(istream& in, Date& d)
{
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
return in;
}
测试:
为了确保用户不会输入一些不合理的日期,例如 2023/29,我们可以写一个成员函数,检查日期是否有效
bool CheckInvalid()
{
if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDays(_year, _month))
return false;
return true;
}
将 CheckInvalid 加到流提取重载中,用于检测用户输入
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
if (d.CheckInvalid())
break;
else
cout << "输入日期无效,请重新输入日期" << endl;
}
return in;
}
测试:
代码
Date.h
#pragma once
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
Date* operator&()
{
return (Date*)0x11223344;
}
// < == > != <= >=
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);
// d+10
Date& operator+=(int x);
Date operator+(int x);
// d-10
Date& operator-=(int x);
Date operator-(int x);
// ++
Date& operator++();
Date operator++(int);
// --
Date& operator--();
Date operator--(int);
// d1 - d2
int operator-(Date& d);
// <<
friend ostream& operator<<(ostream& out, const Date& d);
// >>
friend istream& operator>>(istream& out, Date& d);
int GetMonthDays(int year, int month)
{
static int days[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 days[month];
}
bool CheckInvalid()
{
if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDays(_year, _month))
return false;
return true;
}
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
// <<
ostream& operator<<(ostream& out, const Date& d);
// >>
istream& operator>>(istream& out, Date& d);
Date.cpp
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// < == <= != > >+
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)
{
if (_day < d._day)
return true;
}
}
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);
}
// d+10
Date& Date::operator+=(int x)
{
_day += x;
// 进位处理
while (_day > GetMonthDays(_year, _month))
{
_day -= GetMonthDays(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int x)
{
Date tmp(*this); // 拷贝构造
tmp += x;
return tmp;
}
//Date Date::operator+(int x)
//{
// Date tmp(*this); // 拷贝消耗
// tmp._day += x;
//
// // 进位处理
// while (tmp._day > GetMonthDays(tmp._year, tmp._month))
// {
// tmp._day -= GetMonthDays(tmp._year, tmp._month);
// tmp._month++;
// if (tmp._month == 13)
// {
// tmp._month = 1;
// tmp._year++;
// }
// }
// return tmp;
//}
//
//Date& Date::operator+=(int x)
//{
// *this = *this + x; // 调用+,而+有拷贝消耗
// return *this;
//}
// d-10
Date& Date::operator-=(int x)
{
_day -= x;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDays(_year, _month);
}
return *this;
}
Date Date::operator-(int x)
{
Date tmp(*this);
tmp -= x;
return tmp;
}
// ++
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
//--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// d1-d2
int Date::operator-(Date& d)
{
Date max(*this);
Date min(d);
int flag = 1;
if (d > *this)
{
max = d;
min = *this;
flag = -1;
}
int cnt = 0;
while (min != max)
{
min++;
cnt++;
}
return cnt * flag;
}
// <<
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day;
return out;
}
// >>
istream& operator>>(istream& in, Date& d)
{
while (1)
{
cout << "请输入年月日:";
in >> d._year >> d._month >> d._day;
if (d.CheckInvalid())
break;
else
cout << "输入日期无效,请重新输入日期" << endl;
}
return in;
}
const成员
当我们使用 const 修饰一个对象时,表示这个对象不可修改,当 const 对象调用成员函数时,成员函数也必须是 const 修饰的成员函数,不然会报错
const Date d1(2024, 1, 1);
d1.Print();
原因如下:
- d1 调用 Print() 时会自动传 this 指针,类型是 const Date*
- 而 Print() 是普通成员函数,参数类型是 Date*
- const Date* 表示指向的数据不可写,只可读;而 Date* 表示数据可读可写,这就造成了权限的放大
- 权限只可平移、缩小,不可放大
什么是权限的平移、缩小、放大呢?
所以 const 对象只能调用 const 成员函数,如下:
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
被 const 修饰的成员函数就是const成员函数,实际上 const 修饰的是this 指针,表示在该函数中不可以对类的成员进行修改
思考:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数中可以调用其他非const成员函数吗?
- 非const成员函数中可以调用其他const成员函数吗?
回答:
- 不可,权限放大
- 可以,权限缩小
- 不可,权限放大
- 可以,权限缩小
总结
- 对于不需要修改成员变量的成员函数,建议 const 修饰,这样 const对象和非const对象 都可以使用
- 对于需要修改成员变量的成员函数,不用加 const,否则不能修改成员变量
取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
class Date
{
public:
Date* operator&()
{
return (Date*)0x11223344; // 假地址
}
}
return out;
}