运算符重载
本章思维导图:
注:本章思维导图对应的xmind
文件和.png
文件都已同步导入至”资源“
1. 运算符重载的意义
我们都知道,对于内置类型我们是可以直接用运算符直接对其进行操作的,但是对于自定义类型,这种做法是不被允许的。
例如对于Date
类:
Date d1;
Date d2(2023, 11, 3);
d1 == d2;
//会报错:error C2676: 二进制“==”:“Date”不定义该运算符或到预定义运算符可接收的类型的转换
因此,为了解决自定义类型不能使用操作符的问题,C++就有了运算符重载
2. 函数的声明
一般来说,运算符重载的函数最好声明在类里面,这样就可以使用被private
修饰的成员变量
声明方式:
operator 运算符 形参列表
- 函数的返回值应该和运算符的意义相对应。例如对于比较运算符
>、==
就应该返回bool
值,对于+、-
就应该返回当前类类型- 函数的形参个数应该和运算符的操作数相对应
例如,我们要声明定义一个Date += 整数
的函数,那我们就要重载运算符+=
:
//根据+=运算符的意义,其返回值应该是操作数本身,因此返回其引用
Date& operator +=(int day)
{
//仅作为运算符重载如何声明定义的演示
//实现细节先不做说明
return *this;
}
2.1 声明运算符重载的注意事项
有小伙伴可能注意到:为什么Date& operator +=(int day)
的形参只有一个day
,不是说形参个数要和运算符的操作数对应吗?
我们不能忘记:在类的成员函数中,都有一个默认的形参this
指针来指向当前的对象。
同时还应该注意以下几点:
- 不能通过连接其他符号来实现重载,例如
operator @()
是不被允许的- 不能修改内置类型运算符的意义
.* . :? sizeof ::
这五个运算符是不可以重载的- 对于二元运算符(操作数为2的运算符),形参列表的第一个参数为左操作数,第二个参数为右操作数。因此对于声明在类里面的重载函数,
this
指针永远是左操作数
3. 函数的调用
在声明定义好运算符重载后,有两种调用方法:
第一种——像内置类型一样直接使用运算符:
d1 += 10;
//调用Date& operator +=(int day)函数,在对象d1的基础上加10
第二种——像调用函数一样,使用运算符重载:
d1.operator+=(10);
//调用Date& operator +=(int day)函数,在对象d1的基础上加10
4. const成员函数
我们来看下面的代码:
class Date
{
public:
Date(int year = 2023, int month = 11, int day = 3)
{
//简单的值拷贝
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << ';' << _month << ' ' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;
d1.Print(); //Yes
d2.Print(); //Not
return 0;
}
//d2.Print()会报错:不能将“this”指针从“const Date”转换为“Date &”
这是因为:
我们之前提到过,引用不能涉及到权限的放大,
const Date d2
对应的是const Date* this
,而函数Date* this
,这就涉及到了权限的放大。
因此,为了解决被const
修饰的对像不能调用成员函数的问题,我们可以用const
来修饰成员函数,这样,本质上成员函数的this
指针就被const修饰了,从而也就可以被const
对象调用。
4.1 const成员函数的声明
将一个成员函数变为
const
成员函数,只要在该函数声明的最后加上const
修饰符即可
例如对于上面提到的Pinrt()
成员函数,将其变为const
成员函数即:
void Print() const
{
cout << _year << ';' << _month << ' ' << _day << endl;
}
4.2 注意点
应该知道,引用不能涉及权限的放大,但是可以进行权限的平移和缩小。
因此:
const
对象可以调用const
成员函数非const
对象也可以调用const
成员函数
但并不能说可以将所有的成员函数都声明为const
成员函数,因为对于有些成员函数,它需要在函数内部修改对象的成员变量。
但是对于那些不需要修改对象成员变量的成员函数,建议都将其声明为cosnt
,这样const
对象和非const
对象都能调用
5. 两个默认成员函数
运算符重载中也有两个成员函数,分别是:赋值=
运算符重载和取地址&\const &
运算符重载
5.1 赋值运算符重载
赋值运算符重载是类的默认成员函数
5.1.1 函数的声明
- 因为赋值运算符重载是类的默认成员函数,因此必须声明在类的里面
=
运算符的操作对象为2,因此该函数有两个形参。第一个形参为被隐藏的this
指针,第二个形参就是要赋予值的对象,且该对象最好被const修饰- 为了支持连续赋值,函数的返回值应该是类类型的引用
例如,声明一个Date
类的赋值运算符重载:
Date& operator=(const Date& d)
{
//
}
5.1.2 函数的定义
应该清楚,赋值实际上也是一种拷贝。而拷贝又分为深拷贝和浅拷贝:
浅拷贝:
- 浅拷贝又称值拷贝
- 浅拷贝只是对成员变量值的简单复制,而不是复制指向的动态分配的资源(如堆内存)
- 原对象和拷贝对象将共享相同的资源。
深拷贝:
- 深拷贝又称址拷贝
- 相较于浅拷贝只是对成员变量值的简单赋值,深拷贝会复制对象的成员变量以及指向的资源,包括指针指向的数据
- 这确保了原对象和拷贝对象拥有彼此独立但内容相同的资源副本。
关于深浅拷贝更为清楚的解释请移步👉C++——拷贝构造
既然拷贝分为深浅拷贝,那么我们的赋值运算符重载也应该分为值拷贝、址拷贝
这两种情况:
值拷贝:
例如对于
Date
类,里面没有指针指向空间资源,因此只需要对其成员变量进行简单复制操作Date& operator=(const Date& d) { //简单的值拷贝 _year = d._year; _month = d._month; _day = d._day; return *this; }
址拷贝:
例如对于
Stack
类,里面有一个指针用于指向栈存储的空间,因此需要进行深拷贝来创建一个和源对象内容相同但地址不同的空间Stack& operator=(const Stack& st) { _capacity = st._capacity; _top = st._top; //深拷贝 _a = (int*)malloc(sizeof(int) * _capacity); if (nullptr == _a) { perror("malloc"); exit(-1); } memcpy(_a, st._a, sizeof(int) * _capacity); return *this; }
5.1.3 赋值运算符重载和拷贝构造的区别
看完上面函数的定义,有小伙伴就肯定会有疑惑了:
这看起来二者之间没有什么区别嘛,为什么要做区分?
我们来看下面的例子:
class Date
{
public:
//构造
Date(int year = 2023, int month = 11, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2 = d1; //拷贝构造还是赋值运算符重载???
Date d3;
d3 = d2; //拷贝构造还是赋值运算符重载???
return 0;
}
各位认为,上面问题的答案是什么?让我们来进行调试:
可以得出结论:赋值运算符重载和拷贝构造的不同之处在于:
- 拷贝构造注重的是
构造
,即用一个已经存在的对象来实例化构造一个新的对象- 赋值运算符重载注重的是
赋值
,即对两个已经存在的对象进行赋值操作
5.1.4 默认赋值运算符重载
和其他默认成员函数一样,如果自己没有声明和定义赋值运算符重载,编译器会自动生成一个
和拷贝构造一样,默认运算符重载只会对成员变量进行简单的值拷贝(浅拷贝),因此如果类的成员变量有指向内存空间的指针,仅依赖系统默认生成的赋值运算符重载是不够的,需要自己声明定义,来实现深拷贝
5.2 &和const &运算符重载
这两个函数都是类的默认成员函数
- 他们的作用仅用于返回对象的地址
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这两个默认成员函数一般用编译器自动生成的默认&
运算符重载即可,不需要自己声明和定义
6. 流插入<<、流提取>>运算符重载
通过查阅资料我们可以得知:cout
是ostream
类的对象,cin
是istream
类的对象
extern ostream cout;
extern istream cin;
6.1 函数的声明
我们可以先看一个例子来推出这两个重载函数的返回值是什么:
cout << 10 << 20;
这串代码实现的是向屏幕连续输出两个数字
10和20
,<<
运算符的运算顺序应该是从左到右,而为了能够实现连续的cout
,我们可以假象在执行完cout << 10
后有生成了一个cout
来完成cout << 20
而为了能够实现执行完cout << 10
后又生成一个cout
,cout
运算符重载的返回值就应该是ostream
类的引用,即ostream &
同理,cin
运算符重载的返回值就应该是istream &
接下来的问题是,这两个运算符重载应该放在类里面还是类外面呢?
如果声明在类里面:
class Date { public: //构造 Date(int year = 2023, int month = 11, int day = 3) { _year = year; _month = month; _day = day; } ostream& operator<<(ostream& _cout) { _cout << _year << ' ' << _month << ' ' << _day << endl; return _cout; } private: int _year; int _month; int _day; };
那当我们使用时:
int main() { Date d1; cout << d1; return 0; } //就会报错:二元“<<”: 没有找到接受“Date”类型的右操作数的运算符(或没有可接受的转换)
这是因为,我们在前面就说过,对于二元运算符,第一个形参就是左操作数,第二个形参就是右操作数。而对于类的成员函数,
this
指针就是第一个参数,即左操作数。因此对于流插入运算符<<
,左操作数就是d1
,右操作数就是_cout
。正确的写法应该为:
d1 << cout;
显然,这种写法的可读性是极差的。我们**应该想办法使
_cout
成为左操作数,this
指针成为右操作数,**但是对于类的成员函数,形参的第一个都必定是指向对象的this
指针,所以,为了达到要求,我们应该将<<
和>>
运算符的重载声明定义在类的外面即:
class Date { public: //构造 Date(int year = 2023, int month = 11, int day = 3) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << ' ' << d._month << ' ' << d._day << endl; return _cout; }
我们将<<
和>>
运算符的重载声明在类外面,可读性的问题是解决了,但是这样就访问不了被private
修饰的成员变量了呀。
为了解决这个问题,我们可以使用友元函数friend
将非类成员函数的函数声明放在类里面,再在声明前面加上修饰符
friend
,这样这个函数就变成了友元函数。
- 友元函数不是类的成员函数
- 但是可以直接访问类的私有成员
例如,对于上面的代码:
class Date
{
public:
//构造
Date(int year = 2023, int month = 11, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
friend ostream& operator<<(ostream& _cout, const Date& d);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << ' ' << d._month << ' ' << d._day << endl;
return _cout;
}
这样,我们就可以正确使用<<
和>>
运算符重载了。
7. Date类的实现
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
int nums[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)))
return nums[month] + 1;
else
return nums[month];
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
if (year < 1 || month < 1 || month > 12 || day < 1 || day > GetMonthDay(year, month))
{
cout << "输入非法" << endl;
exit(-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)
{
if (day < 0)
{
return *this -= (-day);
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_month = 1;
_year++;
}
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
// 日期-天数
Date operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
// 日期-=天数
Date& operator-=(int day)
{
if (day < 0)
return *this += (-day);
_day -= day;
while (_day < 1)
{
_month--;
if (_month <= 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// 后置--
Date operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d)
{
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;
else
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
// >=运算符重载
bool operator >= (const Date& d)
{
return (*this == d) || (*this > d);
}
// <运算符重载
bool operator < (const Date& d)
{
return !(*this >= d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
return (*this == d) || (*this < d);
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
int operator-(const Date& d)
{
int flag = 1;
if (!(*this >= d))
flag = -1;
Date cmp_big = *this >= d ? *this : d;
Date cmp_small = *this < d ? *this : d;
int count = 0;
while (cmp_big != cmp_small)
{
count++;
++cmp_small;
}
return flag * count;
}
// 析构函数
~Date()
{
//cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
7.1 对于部分代码的说明
-
注意代码的复用,这样可以少些许多代码,提高效率。
例如:定义好
+=、-=
的运算符重载后,-、+
运算符的重载就可以复用+=、-=
的代码;定义好==、>
的运算符重载后,其他比较运算符就可以复用==、>
的运算符重载来实现,从而大幅减少了代码量。 -
注意前置
++
和后置++
的运算符重载(前置--
和后置--
同理)由于C++规定,要构成运算符重载,运算符必须跟在
operator
后,因此当碰到前置++和后置++这种运算符相同(函数名相同)但函数实现的功能不同的情况时,就需要利用函数重载。我们可以在后置++
的运算符重载函数的形参列表里加入一个形参,和前置++
构成函数重载,这样就可以避免问题了。