文章目录
- 前言
- 一、运算符重载的概念和意义
- 二、运算符重载的规则
- 三、常用运算符重载
- 1.关系运算符重载
- 2.=赋值运算符重载
- 3.+=、-=、+、-重载
- 4.前置++和后置++重载
- 5.流插入<<和流提取>>重载
前言
之前在总结类的六个默认成员函数时,没有过多介绍运算符重载,只简单介绍了赋值运算符重载。本节内容将会总结常用的运算符重载,以实现一个日期类为例。
一、运算符重载的概念和意义
什么是运算符重载?
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时做出不同的行为。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
运算符重载的语法格式
返回值类型 operator运算符(参数列表)
{
函数体
}
注:这里的运算符可以是+、-、*、/、>、>=等,但不能创建新的运算符如@、$等。
.*
::
sizeof
?:
.
这5个运算符不支持重载。
二、运算符重载的规则
以下面一个日期类为例
class Date
{
public:
Date(int year = 1, 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; }
判断是否相等的运算符==是双目运算符,所以参数列表有两个参数。因为传值传参会调用拷贝构造,降低效率,所以用传引用传参;
因为不改变实参,所以参数最好加上const;成员函数后面加const,对于不改变成员变量的成员函数,我们最好在函数名后面加上const,提高代码健壮性。
但是我们如果重载成全局函数,就需要类成员变量是公有属性,因为类外无法访问类的私有成员。但是这样就会破坏封装性。 因此,为了保证封装性,我们一般将运算符重载为类的友元函数或类的成员函数。
- 重载为类的友元函数(全局函数)
friend bool operator==(const Date& d1, const Date& d2);
函数定义不变,只需要在类中加上友元的声明,就可以正常使用上述函数,还不会破坏类的封装性。
- 重载为类的成员函数
bool operator==(const Date& d) const { return _year == d._year && _month == d._month && _day == d._day; //等价于 //return this->_year == d._year && this->_month == d._month && this->_day == d._day; }
可以看到,参数个数减少了一个,这是为什么呢?
答:这是由于类的每个非静态成员函数都有一个隐藏的this指针,占第一个参数的位置,也就是说,上述写法表面上是一个参数,实际上有两个参数。如下,但我们定义时不需要显示地传this指针。//等价于,但不能这样写 bool operator==(Date* this, const Date& d2) const
注:对于不改变this的成员函数,我们最好在函数名后面加上const,变成常成员函数,增加代码健壮性。
总结:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
运算符重载的调用方法如下,和之前调用方法一样。
int main()
{
Date d1(2024, 6, 25);
Date d2(2024, 6, 24);
//d1 == d2
//重载为友元函数,等价于if(operator==(d1, d2))
//重载为成员函数,等价于if(d1.operator==(d2))
if (d1 == d2)
{
cout << "d1 == d2" << endl;
}
return 0;
}
运算符重载规则:
1.只能对已有的运算符进行重载,不可以创建新的运算符。
2.重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语法结构。
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
5..*
::
sizeof
?:
.
这5个运算符不支持重载。
6.赋值=
、下标[]
、调用()
、成员访问->
,这4个运算符必须重载为类的成员函数,不能重载为全局函数。
4.若一个运算符的操作需要修改对象的状态(修改this指针),最好重载为类的成员函数。
三、常用运算符重载
1.关系运算符重载
总共有==、!=、<、<=、>、>=这六个关系运算符,进行对象之间的比较判断,所以返回值为false或者true,即bool类型。
这里我将关系运算符重载为类的成员函数(也可以重载为类的友元函数)
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
//函数重载声明
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;
private:
int _year;
int _month;
int _day;
};
//定义
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)
{
if (_month == d._month)
return _day < d._day;
else
return _month < d._month;
}
else
{
return _year < d._year;
}
}
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;
}
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
bool Date::operator>=(const Date& d) const
{
return !(*this < d);
}
重载之后我们就可以比较类类型对象的大小。要理解关系运算符的对应关系,比如实现了=
和<
重载,我们可以借此来更简单地实现<=
、>
等。
2.=赋值运算符重载
前面总结类的六个默认成员函数时,已经介绍过赋值运算符重载。赋值运算符重载只能重载为类的成员函数。
赋值运算符重载用在两个及以上已存在的对象之间进行赋值。分清这点与拷贝构造(用已存在对象初始化新对象)的区别。
赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率;
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
返回*this :支持连续赋值;
检测是否自己给自己赋值;
日期类的赋值运算符重载(可以不用写)
Date& operator=(const Date& d)//传引用提高传参效率
{
if (&d != this)//判断优化
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
赋值运算符重载也是类的默认成员函数,我们不写,编译器会生成一个默认赋值运算符重载,完成数据的浅拷贝。
对于日期类这种成员变量都是内置类型的类,我们不需要显示定义赋值运算符重载,使用编译器默认生成的就可以了。但是涉及到资源申请的比如栈这种,浅拷贝就无法满足,需要我们自己显示定义赋值运算符重载完成深拷贝。
3.+=、-=、+、-重载
对于+=
和-=
运算符,我们知道,这两个运算符会改变对象自身,且返回修改后的对象。
+= 的第二个操作数有可能是负数,-=的第二个操作数有可能是正数,因此先进行判断,情况要考虑周全。
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 > 12)
{
_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;//返回对象本身
}
实现+=和-=后,+和-就可以套用了。当然也可以先实现+和-,再套用实现+=和-=
Date Date::operator+(int day) const
{
Date tmp(*this);
tmp += day;
return tmp;//出了作用域销毁,不能用引用返回
}
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= 1;
return tmp;//出了作用域销毁,不能用引用返回
}
上述函数重载都是对日期进行指定天数的加减运算,那如何进行日期与日期间的运算呢?运算符重载不能实现无意义的重载,比如两个日期相加,是不符合逻辑的;但两个日期是可以相减的,返回两个日期的差值。
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int cnt = 0;
while (max != min)
{
cnt++;
min++;
}
return cnt * flag;
}
这里的减-
与之前的减-
构成重载关系。
注意:
1.
+=
和-=
会改变对象本身,且返回*this即对象本身,所以可以用引用返回提高效率。
2.+
和-
并不会改变对象的值,返回的是对象进行加减运算后的值(临时变量),不能用引用返回。
4.前置++和后置++重载
前置++(- -)和后置++(- -)都是单目运算符,如果重载为类的成员函数,则是无参的。
为了让前置++与后置++能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。 也就是说,二者的调用还是和内置类型调用的方法一样,只不过定义重载时后置++的参数多个无意义的int参数。
由于前面已经实现了+=的功能,这里不再重复了,直接套用即可。
//前置++ Date& Date::operator++() { *this += 1; return *this; } //后置++ Date Date::operator++(int)//int无任何意义,只是为了区分前置还是后置 { Date tmp(*this); *this += 1; return tmp; }
调用方法
int main() { Date d1(2024, 6, 25); ++d1;//等价于d1.operator++(); d1++;//等价于d1.operator++(0); return 0; }
调用后置形式的重载函数时,对于那个没用的 int 形参,编译器自动以 0 作为实参。
前置++: 返回+1之后的结果,this指向的对象再函数结束后不会销毁,所以用引用返回提高效率。
后置++: 先使用后+1,因此需要返回+1之前的旧值,在实现时需要先将原始对象保存一份,然后*this+1;注意临时对象只能以值的方式返回,不能返回引用。
前置- -与后置- -也是同理,这里不再具体实现。
5.流插入<<和流提取>>重载
C++标准库对左移运算符<<
和右移运算符>>
分别进行了重载,与cout和cin搭配使用,使其能够用于不同数据的输入输出。
int i = 0; double d = 1.23; cout << i; cout << d;
<<和>>可以直接支持内置类型是因为C++标准库里已经实现好了,我们可以直接使用;
可以直接支持自动类型识别是因为函数重载。
对于内置类型,我们可以直接使用,但对于自定义类型,我们需要重载这两个操作符。
比如对于日期类,我们重载这两个运算符后,可以实现输入和输出年月日。
Date d1;
cin >> d1;
cout << d1;
通过查阅C++官网资料,我们知道,cin是istrem类的对象,cout是是ostrem类的对象,这两个都在
<iostream>
头文件中;因为C++标准库中istrem类和ostrem类重载了内置类型的参数,所以内置类型可以直接使用并且可以自动识别类型。
为什么输入输出操作符要重载为友元函数,不能重载为成员函数?
我们知道,成员函数的第一个参数是隐藏的this指针,如果我们重载为成员函数,也就是说,this指针为左操作数,cout/cin为右操作数,那么就不符合我们常规的调用顺序,不符合使用习惯。
ostream& operator<<(ostream& out)
{
out << _year << "-" << _month << "-" << _day << endl;
return out;
}
//调用
int main()
{
Date d1, d2;
//与我们常规调用顺序相反cout << d1;
d1 << cout;//d1.等价于operator<<(cout)
return 0;
}
实际使用中cin/cout为第一个形参对象,才符合常规使用。所以要将这两个运算符重载成全局函数。但又会导致类外没办法访问非公有成员,就需要借助友元来解决。
class Date { friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); public: Date(int year = 1, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day << "-"; return out; } istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; } int main() { Date d1, d2; cin >> d1 >> d2;//等价于operator>>(operator>>(cin, d1), d2); cout << d1;//等价于operator<<(cout, d1); return 0; }
为什么要有返回值? 为什么返回第一个参数的引用?
答:返回isteam/ostream类对象的引用作为下次调用时的左操作数,是为了能够连续读取。