[toc]
主要是代替成员函数的方式为自建类型完成基本任务
当然, 用成员函数完全可以代替operator的功能, 但是使用起来绝对没有operator方便
既然是用于自建类型的运算, 则其可以有两种定义方式:
作为自建类型的成员函数, 定义在类的内部
此时operator的参数数目比具体重载的运算符操作数数目少一, 因为此时使用的一个隐含参数为* this, 并将其作为左操作数(第一个操作数)
如果需要将* this作为右操作数, 只能将operator作为友元函数
作为自建类型的友元函数, 定义在类的内部或外部
此时operator的参数数目与具体重载的运算符操作数数目相同
对于两种方式的选择, 需根据具体的需求而定, 但有以下几点准则可以提供参考:
说白了, operator能重载的只有运算符对操作数的操作, 而其他东西基本不能改变:
operator重载相应的运算符时仍然需要遵守其原定的语法, 不能将双目运算符重载为单目运算符(实际上这也是编译器判定参数的一个标准), 也不能修改运算符的优先级
operator重载运算符时不能覆盖原有的运算, 即操作数中必须至少有一个是自建类型, 这虽然限制了一点操作性, 但保护了程序的正常执行
不能创建新的运算符, 只能重载原有的运算符, 如operator ** //非法
必须作为类成员的运算符重载:
C++规定,= 赋值运算符, []下标运算符, ()函数调用运算符, ->成员访问运算符
只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数
就记着着四个奇葩不能静态不能友元
因为:
对于static静态成员函数, 由于没有this指针, 只能访问类的静态成员, 这导致无法对类对象进行操作
对于友元函数: 编译器在类中寻找是否存在用户自建的operator= 时, 判定条件为是否有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数, 而友元函数不属于这个类, 所以此时编译器相当于没找到, 所以会合成默认的operator=
这样, 在调用的时候会造成冲突, 所以C++限制了operator= 的重载
重载运算符无法保留一些运算符原有的一些特性:
所以不推荐对这些运算符进行重载, 否则可能重载后的使用规则发生变化会导致一些使用上的Bug
包括上头的&& || , 还有 & 取地址运算符, 因为在C++中已经定义了其对类对象的操作, 重载该运算符会导致丧失一部分功能, 为类的使用者带来麻烦
重载运算符最需要考虑的即为参数与返回值问题
(这里以operator = 为例):
一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用, 如
MyStr& operator =(const MyStr& str);
加const是因为:
用引用是因为:
注意:
上面的规定都只是推荐,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象,正如后面例2中的那样
一般地,返回值是被赋值者的引用(但有时返回左值还是右值需要相当的考虑),即*this
MyStr& operator =(const MyStr& str);
这样在函数返回时避免一次拷贝,提高了效率。
更重要的,根据赋值运算符的从左向右的结合律, 可以实现连续赋值,即类似a=b=c
如果返回的是值,则执行连续赋值运算后后头得到的将是一个匿名副本, 为不可更改的右值, 再执行=c就会出错。
注意:
这也不是强制的,完全可以将函数返回值声明为void,然后什么也不返回,只不过这样就无法连续赋值
所以具体的返回值与参数的设定完全取决于需求, 是非常值得设计者考量的
当使用运算符操作非基本对象时, 编译器会根据调用的运算符和操作数的类型自动查找对应的重载运算符函数, 如果找不到则会Error
需要注意的特殊情况:
class MyStr str2;
str2 = str1;
//注意这两种方式不同, 前者是赋值运算, 后者是拷贝构造
class MyStr str3 = str2;
运算符的操作在类对象的定义中通常被编译器link到类的初始化, 从而与真正的运算符重载函数无缘
对于单目运算符的重载, 也遵循上头的重载运算符限制, 即不能将负号-重载为后置, 编译器也依据此来选择合适的参数
而对于具有前置&后置版本的单目运算符, 如自增运算符++, 其重载有特定的要求:
student& operator++(void){
//前置++重载
++this->num;
return *this;
}
student operator++(int){
//后置++重载
class student temp(*this);
++this->num;
return temp;
}
对于前置++的重载, 要求其参数列表为空
对于后置++的重载, 要求其参数列表有一个参数, 且必须是int (有博客有说可以是任意类型的, 但我使用Qt的MinGW编译报错), 其作用是为了区别前置++的重载版本, 同时告诉编译器这个运算符后头是有参数的, 这样编译器才知道这是后置版本
编译器在调用后置版本的重载运算符时会给int形参传入一个0, 但是一般不会用到这个值, 所以通常不提供标识符
由于IO操作通常需要读写类对象的成员, 且自建类成员通常作为右操作数, 所以重载运算符一般设置为friend友元函数
这类重载函数通常需要使用C++ IO库的成员, 而IO对象无法被拷贝或赋值, 所有的操作只能通过指针或引用来进行, 所以函数的参数和返回值通常为IO对象的引用
并且, 由于向流写入或读取内容会导致流的状态发生改变, 所以重载运算符中无法使用const类型的IO成员
针对<<的重载运算符:
通常情况下, 由于输出时不修改右操作符, 所以将<<重载的第二个参数设置为const类型
//由于类对象作为右操作数, 所以使用友元函数的形式
friend ostream& operator<<(ostream &o,const student &x){
cout<<"operator <<"<<endl;
o << x.num;
return o;
}
针对>>的重载运算符:
由于输入的特殊性, 在重载运算符函数中有必要考虑可能的输入失败的情况并作出补救措施(如重置成初始状态)
判断方法可以通过IO成员内置的标识符来判定(在Chapter. VII中)
常见的错误有:
[拓展: ]好的编程习惯
在类对象的输出中, 应该尽可能少的进行格式化操作, 而将这个任务交个类的使用者, 使其可以更加自如的使用类
当重载函数调用运算符时, 并非创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数, 而其相应的类被称作 "函数对象"
即可以像使用函数一样直接使用类对象, 并向其传递特定数目和类型的参数, 编译器会调用不同的重载函数完成相应的操作, 并且是唯一一种支持形参缺省值的运算符重载
通常, 函数对象中的数据成员被用于定制operator()函数调用运算符中的一些操作, 并且函数对象经常作为泛型算法的实参被传递(这个先等等…水很深的…)
总之函数调用运算符的重载拓宽了设计者的创作空间
//类内定义:
void operator () (int n1=0,int n2=0){
//支持提供缺省值的方式
cout<<"Operator () Overload\n";
num=n1+n2;
return ;
}
//使用:
class student stuObj1(student(250));
stuObj1(1,2);
cout<<stuObj1<<endl;
输出结果:
Overload Constructor: int
Operator () Overload
operator <<
3
前排提醒: 这个运算符挺坑的
注意, 箭头运算符与解引用运算符一样都是单目运算符, 尽管其看起来像双目运算符, 但其右操作数不是表达式, 而是对应类成员的一个标识符, 编译器将通过此标识符获取特定的成员
//pointer为指向类对象的指针. 此时两条语句等效
pointer->member; //编译器调用的是-> 的内置版本, 与重载版本无关
*(pointer).member;
//pointer为某个类的对象时:
pointer->member; //此时编译器调用的才是重载运算符版本
//相当于:
pointer.operator->() ->member; //对, 就是这么一个奇葩玩意
//相当于调用了->的重载函数, 并将它的返回值在做了一次->运算
此时有两种情况:
所以, 想要operator->退出, 只能通过第一种情况, 否则他会在两种情况中不断的变换, 要么无限递归, 要么报错
所以如果operator->返回的是其类对象的引用, 则会无限递归, 编译器报错:
#include
class myClass {
public:
myClass& operator->() {
cout<<"Operator -> Overload\n";
return *this;
}
void action() {
//do something...
return;
}
};
int main() {
myClass obj;
obj->action();
return 0;
}
直接报错:
error: circular pointer delegation detected
重载运算符对类服务, 自然也要从类的整体设计上考虑何时应该重载运算符, 何时应使用成员函数:
通常, 当类的某些操作在逻辑上与对应的运算符相关, 则其更应该设置为重载运算符的形式:
如果没有特殊的需要, 重载这些运算符时参数与返回值的设置最好与内置版本相兼容, 符合用户的使用习惯, 更不容易发生错误
重载运算符最本质的目的是为了方便使用
一切的方便都是建立在对原有的运算符功能的理解上的
如果一个操作在功能上存在一定的二义性, 或者与常规的理解存有一定的偏差, 则此时不应该扭曲原有运算符的含义与逻辑, 转而用一个成员函数并在函数名上给出提示更能方便使用
运算符重载在C++中同样作为函数来调用, 所以仍然遵循函数的匹配模式
但总体上, 运算符重载函数的候选集合比普通重载函数要来的大:
当运算对象中有类类型时, 函数匹配列表中的候选函数应该包括该运算符的非成员重载版本和内置版本, 且如果左侧的运算符是类类型, 则还要包括该类中定义的成员函数重载版本