目录
前言
本文主要内容是关于类中生成的三个默认成员函数,还有日期类的实现。干货满满,一起学起来吧!
1. 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通函数类似。
函数名字:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。
如下面代码所示,如果我们将运算符重载成全局函数,那么类中的成员变量权限必须是公有地,这样运算符重载才能访问类中的成员变量。可是成员变量的权限变为公有的,类的封装性如何保证?有三种解决方法:
- 提供这些成员变量的Get和Set函数(一个是获取成员变量,另一个修改成员变量)。
- 使用友元,可以使全局函数访问类中私有的成员。
- 运算符重载成类的成员函数(最推荐的做法)。
// 全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//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;
}
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
下面是重载成类的成员函数的代码,注意重载成成员函数,默认第一个参数是Date* const this,表示第一个类对象的指针,其中指针this是不能修改,即this的指向不能改变。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* const this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
2. 赋值重载函数
1. 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要符合连续赋值的含义
class Date
{
public:
Date(int year = 1900, 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;
}
Date& operator=(const Date& d)
{
if (this != &d)//判断自己给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 20);
Date d2(d1);//拷贝构造
Date d3;
d3 = d1;//赋值拷贝构造
Date d4;
d4 = d2 = d1;//连续赋值,是因为赋值重载函数返回值是Date&
return 0;
}
赋值操作中也会出现,d1=d1,自己给自己赋值的情况。于是需要判断this指针存放的地址和传进来的参数是否不同,不相同才进行赋值,想通直接返回*this。
2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
//编译错误
原因:赋值运算符如果不显示实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算赋重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员需要调用对应类的赋值运算符重载完成赋值。
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 = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?如果是像上面的日期类就没必要。那下面的类呢?试一下看看:
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
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;
}
在VS2022上,就出现这样的报错。这是为什么呢?
- 一开始,s1调用构造函数申请了10个元素空间,并存储1,2,3,4这四个值。s2也调用了构造函数申请了10个元素的空间,没有存储值。这两个对象中的_array成员变量开辟空间的地址是不同的。
- 第二部分,当s2调用赋值运算符重载,拷贝s1。由于Stack没有显示实现复制运算符重载,编译器会以浅拷贝的方式,将一个类对象原封不动的拷贝到另一个对象中。这会导致两个问题。
- 如下图所示,由于进行按字节拷贝,s2原来中的_array指针存储的地址变成s1中_array的地址。则s2原来开辟的空间丢失了,存在内存泄露
- s1和s2共享一份空间,最后销毁的时候会导致同一份空间释放两次而引起程序崩溃。
3. 前置++和后置++重载
前置++:
- 返回+1之后的结果
- 前置++是一元运算符,不需要传递任何参数。
- 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
后置++:
- 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
- C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。
- 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
4. const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
//相当于对隐含的this指针进行了const修饰
//void Print(const Date *this)
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
//cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
来看看下面的代码,思考一下这几个问题。
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
class Date
{
public:
Date(int year, int month, int day)
{
_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; // 日
};
void Test()
{
Date d1(2022, 1, 13);
d1.Print();
const Date d2(2022, 1, 13);
d2.Print();
}
- const对象不能调用非const成员函数,因为const对象被const修饰,无法改变内部的成员变量,调用非const成员函数,可能存在修改const对象的操作,这是权限的放大,不被允许。
- 而非const对象,本身是允许被修改的。如果调用const成员函数,就不能修改,权限缩小了,这是可以的。
- 下面两个问题的分析跟上面的类似。
5. 日期类的实现
5.1 声明和定义分离
实现一个日期类,先创建三个文件Date.h,Date.cpp和test.cpp。
- Date.h文件存放类的声明部分。
- Date.cpp存放类成员函数的实现部分。
- test.cpp用于写测试代码
5.2 日期类的声明
- 日期类需要写一个全缺省的构造函数,析构函数不需要写,因为日期类的内置类型不涉及资源清理的工作。而拷贝构造函数和赋值重载函数编译器会默认生成,进行浅拷贝,这也是日期类是内置类型的内置类型不涉及资源清理的工作。
- 因为日期可以比较大小,先完成日期类比较类操作符的重载。
- 日期不管是加上还是减去天数可以得到新的日期,也有意义。
- 还有前置和后置的加加和减减,最后是日期减去日期,可以得到相差的天数。
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
//打印日期
void Print() const;//const Date* this
// >运算符重载
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;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++Date& operator++();
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
5.3 日期类的比较,构造和打印
- 构造函数写一个全缺省构造函数,也是默认构造函数的一种。
- 打印日期,可以有很多方式,选择其中一种即可。
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
- 日期类的比较,有大于,小于和相不相等的关系。本质上是一种判断,不会涉及到对象内部成员变量的修改,所以都需要在函数声明后面加一个const,变成const成员函数。
- 我们可以先写一个日期类的小于操作符,先比较年份,年份小就返回真。如果年份相等就比较月份,月份小也返回真。如果月份相等,就要比较天数,直接返回两个日期对象的比较。
- 日期的小于操作符实现起来还是比较麻烦的,那大于操作符也需要像上面的思路一样吗?其实可以复用小于操作符,然后取反操作即可。
- 日期类等于操作符的重载,直接返回年月日相等的判断,只要其中一个不相等就返回假。那么不相等操作符的重载可以复用等于操作的重载,取反即可。
- 日期类的大于等于和小于等于操作符的重载就可以复用上面的操作符。
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)
{
return _day < d._day;
}
}
}
}
return false;
}
bool Date::operator>(const Date& d)const
{
return !(*this <= d);
}
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
{
return *this < d || *this == d;
}
bool Date::operator>=(const Date& d)const
{
return !(*this < d) || *this == d;
}
5.4 日期类的加减天数
日期加上一个整型,会得到一个新的日期。那么实现这个+=操作符重载,我们的思路像整数相加的进位类似。
- 先让日期加上天数,得到一个整数,如果得到的整数大于该月的天数,月份就加1。然后将整数减去该月的天数。如果该月份加一等于13,说明这一年已经过完了,年份就要加1,并重置月份为1。直到这个整数小于该月的天数即可。
- 首先,我们要解决如何获得该月天数的问题,需要封装一个函数为GetMonthDay,传递年和月这两个整型变量。
- 因为这个函数会经常调用,所以我们可以直接定义在类中。如果直接定义在类中,默认就是内联函数。其中可以定义一个数组,存放每个月对应的天数。开辟13个整型元素,从下标为1的元素开始存放天数。
- 数组加上static关键字,放在静态区,生命周期跟全局变量相同。使用+运算符重载只需要创建一次,如果不加,每次调用GetMonthDay函数都需要创建这个数组,消耗时间。
完成了+=运算符重载,+运算符重载的内部逻辑跟它相同,可以直接复用它。不过得注意+=运算符,是一个日期对象加天数,返回的是本身,可以用引用类型返回。而+运算符不是一个日期对象本身的操作,需要返回一个新的日期对象,只能类类型返回。
//Date.h
//直接定义类里面,它默认是inline
int GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
//加上static的时候,直接将变量放在静态区,只有第一次调用会开辟,会面复用不会
//如果不加,需要重复创建变量。
static int monthDayArray[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;
}
else
{
return monthDayArray[month];
}
}
//Date.cpp
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;
}
-=运算符重载和-运算符重载的实现也是类似的。
- 先让日期减去传递的整数,日期如果小于0,月份就减1。如果月份减1后等于0,说明这一年不够减了,年份减1,月份重置为12。然后日期加上该月的天数,重复这个循环直到日期大于0为止。
- -运算符重载复用-=运算符重载即可。
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
// TODO: 在此处插入 return 语句
}
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);
(*this) += 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
(*this) -= 1;
return tmp;
}
5.5 日期类相减
两个日期相减会得到它们中间相差的天数,有实际意义。
思路是假设日期较大的是原来的日期对象,较小的是参数中传递过来的日期对象。如果原来的日期对象小于参数传递的日期对象,就改变max和min的赋值情况。再定义的flag赋值为-1,因为原来的日期对象小于参数传递的日期对象,说明被减数大于减数,会得到一个负数。定义一个变量n,来存放相差的天数,我们使用前置++,让min不断加1,直到和max日期相等。最后返回n乘上flag。
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;
}
5.6 测试函数
#include "Date.h"
void TestDate1()
{
Date d1(2024, 4, 14);
d1 += 200;
d1.Print();
Date d2(2024, 4, 14);
d2 -= 14;
d2.Print();
}
void TestDate2()
{
Date d1(2024, 4, 14);
Date d2;
d2 = d1++;
d1.Print();
d2.Print();
Date d4(2024, 4, 14);
Date d3;
d3 = ++d4;
d3.Print();
d4.Print();
}
void TestDate3()
{
Date d1(2024, 4, 21);
Date d2(2100, 5, 10);
int n = d2 - d1;
cout << n << endl;
}
int main()
{
TestDate1();
TestDate2();
TestDate3();
return 0;
}
运行结果如下:
6. 取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,除非你不想别人获取这个类对象真正的地址,让他获取其他的地址。
总结
通过日期类的实现,对C++中类和对象的了解更加深入,开始上手C++。学习编程语言需要不断的练习,多多重复,百炼成钢!
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!