五、运算符重载
1、运算符重载的引入
对于C/C++编译器来说,它知道内置类型的运算规则,比如整形+整形、指针+整形、浮点型+整形;但是它不知道自定义类型的运算规则,比如日期+天数 、日期直接比较大小、日期-日期;我们要进行这些操作就只能去定义对于的函数,比如AddDay、SubDay;但是这些函数的可读性始终是没有 + - > < 这些符号的可读性高的,而且不同程序员给定的函数名称也不一样相同;
所以为了增强代码的可读性,C++为自定义类型引入了运算符重载,运算符重载是具有特殊函数名的函数 – 其函数名为关键字operator+需要重载的运算符符号,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似;换句话说,运算符重载函数只有函数名特殊,其他方面与普通函数一样;我们以日期+天数为例:
Date类:
class Date { public: Date(int year = 1970, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //获取每个月的天数 int GetMonthDay(int year, int month) { static int day[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 day[month]; } //打印 void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; };
函数方式实现:
void AddDay(Date& d, int day) { d._day += day; while (d._day > GetMonthDay(d._year, d._month)) { d._day -= GetMonthDay(d._year, d._month); d._month++; if (d._month > 12) { d._month -= 12; d._year++; } } }
运算符重载方式实现:
void operator+=(Date& d, int day) { d._day += day; while (d._day > GetMonthDay(d._year, d._month)) { d._day -= GetMonthDay(d._year, d._month); d._month++; if (d._month > 12) { d._month -= 12; d._year++; } } }
2、运算符重载函数的位置
如果大家实际上手编写我们上面的 AddDay 和 operator+= 函数就会发现一个问题:类中的成员函数 _year、_month、_day 都是私有的,我们在类外并不能直接修改它们;
但是我们又不能直接把成员变量设为共有,这样类的封装线得不到保证;那么如果我们把函数放到类里面呢?比如下面这样:
上面这种情况是由我们在 类和对象上篇 中提到的 this 指针引起的 – 类的每个成员函数的第一个参数都是一个隐藏的 this 指针,它指向类的某一个具体对象,且 this 不能显示传递,也不能显示写出,但是可以在函数内部显示使用;
也就是说,本来 += 这个操作符只能有两个操作数,所以使用 operator 重载 += 得到的函数也只能有两个参数;但是由于我们为了使用类的成员变量将函数放在了类内部,所以编译器自动传递了对象的地址,并且在函数中使用一个 this 指针来接收,导致函数参数变成了三个;所以出现了 “operator += 的参数太多” 这个报错;
那么为了解决这个问题,我们在定义 operator+= 函数时,就只显式的传递一个参数 – 右操作数,而左操作数由编译器自动传递;当我们在函数内部需要操作左操作数时,也直接操作 this 指针即可;
还是以日期+天数为例;
//运算符重载+= void operator+=(int day) //只传递右操作数,通过this操作左操作数 { this->_day += day; //这里的this->编译器会自动添加 while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month > 12) { _month -= 12; _year++; } } }
注意
1、当我们将函数放在类内部时,不管操作数有几个,this 默认指向第一个操作数;
2、对于在类外部无法访问类的私有成员变量的问题其实也可以使用友元解决,我们后面再学习;
3、运算符重载的特性
运算符重载函数有如下特性:
不能通过连接其他符号来创建新的操作符:比如operator@;
重载操作符必须有一个类类型参数 (因为运算符重载只能对自定义类型使用);
用于内置类型的运算符,其含义不能改变,即不能对内置类型使用运算符重载;
作为类类的成员函数重载时,其形参看起来比操作数数目少1,是因为成员函数的第一个参数为隐藏的 this;
以下5个运算符不能重载: .* :: sizeof . ?: 注意这个经常在笔试选择题中出现,特别是 .* 操作符,希望大家记住;
4、常见的运算符重载
常见的运算符重载有:operator+ (+)、operator- (-)、operator* (*)、operator/(/)、operator+= (+=)、operator-= (-=)、operator== (==)、operator= (=)、operator> (>)、operator< (<)、operator>= (>=)、operator<= (<=)、operator!= (!=)、operator++ (++)、operator-- (–)等;
其中,对于 operator++ 和 operator-- 来说有一些不一样的地方 – 因为 ++ 和 – 分为前置和后置,二者虽然都能让变量自增1,但是它们的返回值不同;但是由于 ++ 和 – 只有一个操作数,且这个操作数还会由编译器自动传递;所以正常的 operator++ 和 operator-- 并不能对二者进行区分;最终,C++规定:后置++/–重载时多增加一个int类型的参数,此参数在调用函数时不传递,由编译器自动传递;
其次,上面重载函数中的 operator= 就是默认成员函数之一 – 赋值重载函数;
注:由于运算符重载函数很多,情况也比较复杂,所以我们将运算符重载的详细细节 (比如引用做返回值、引用做参数、函数的复用、对特殊情况的处理等知识) 放在 Date 类的实现中去介绍;
六、赋值重载
1、基础知识
赋值重载函数是C++的默认六个成员函数之一,它也是运算符重载的一种,它的作用是两个已存在的对象之间的赋值,其特性如下:
- 赋值重载的格式规范;
- 赋值运算符只能重载成类的成员函数不能重载成全局函数;
- 若未显式定义,编译器会生成默认的赋值重载函数;
- 默认的赋值重载函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝,对自定义类型调用其自身的赋值重载函数;
2、特性分析 – 函数格式
赋值重载函数的格式一般有如下要求:
使用引用做参数,并以 const 修饰
我们知道,使用传值传参时函数形参是实参的一份临时拷贝,所以传值传参会调用拷贝构造函数;而使用引用做参数时,形参是实参的别名,从而减少了调用拷贝构造在时间和空间上的消耗;另外,赋值重载只会改变被赋值对象,而不会改变赋值对象,所以我们使用 const 来防止函数内部的误操作;
void operator=(const Date& d);
使用引用做返回值且返回值为*this
我们可以对内置类型进行连续赋值,比如 int i,j; i = j = 0; 那么对于自定义类型来说,我们也可以使用运算符重载来让其支持连续赋值,则重载函数就必须具有返回值;同时,由于我们是在函数外部调用重载函数,所以重载函数调用结束后该对象仍然存在,那么我们就可以使用引用作为函数的返回值,从而减少一次返回值的拷贝,提高程序效率;
另外,我们一般使用左操作数作为函数的返回值,也就是 this 指针指向的对象;
Date& operator=(const Date& d);
检测是否自己给自己赋值
用户在调用成员函数时有可能发生下面这种情况:Date d1; Date& d2 = d1; d1 = d2; 这种情况对于只需要浅拷贝的对象来说并没有什么大碍,但对于有资源申请,需要进行深拷贝的对象来说就会发生不可控的事情,具体案例我们在第四点特性中讲解;
在 《Effective C++》中对赋值重载函数自我赋值的解释是这样的:
if(this == &d) //比较两个对象的地址是否相同 return *this;
Date 类的赋值重载函数如下:
//赋值重载 Date& operator=(const Date& d) { //自我赋值 if (this == &d) { return *this; } _year = d._year; _month = d._month; _day = d._day; return *this; }
3、特性分析 – 重载为成员函数
赋值运算符只能重载成类的成员函数不能重载成全局函数,这是因为赋值重载函数作为六个默认成员函数之一,如果我们不显示实现,编译器会默认生成;此时用户如果再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中生成的默认赋值运算符重载冲突,从而造成链接错误;
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; }; Date& operator=(Date& left, const Date& right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; }