目录
成员访问运算符(通常用于智能指针或是一些有指针的数据成员时):
函数调用运算符(必须为成员函数)函数对象和lambda表达式:
重载运算符的基本概念:
我们不能重载内置类型的运算符,且不能创造一个新的运算符进行重载。正常的重载运算符参数数量应该与其运算对象一样多,而只有重载()运算符外,其他不能有默认实参。如果重载运算符为成员函数,其第一个左侧对象默认为绑定的this指针上。参数数量与运算对象少一个(为默认为this指针)。
调用重载运算符:
间接方式调用:s1+s2;
直接函数调用:operator+(s1,s2);
二者是等价的。如果在成员函数中:
data1+=data2;
data1.operator+(data2);(data1为左侧运算对象)
而有些运算符不应该被重载:
有些运算符有其隐含的运算规则和求值顺序,像&,|有其短路特性,即如果&左侧为假不判断右侧对象的真假,|如果左侧对象为真,则不判断右侧对象的真假。还有逗号运算符。而重载后可能无法保证其求值顺序被保留,还有求址和逗号运算符,其本身就定义了对象为类的操作。故不应该对以上情况重载。
应该保持与内置类型一致的含义:
最好重载相关的运算符实现的功能相似与内置类型使用其的功能。也最好保持其特性一致。像赋值运算符=,赋值后,左侧对象与其右侧对象的值相等,返回左侧对象的引用。因此重载时也要实现此功能并返回左侧对象的引用。或者是+=运算符,应该为先+在赋值,保持其特性。而应该配套运算符相关联的运算符,例如要重载<运算符,应该把相应的比较运算符也重载进去。如果有==,也应该重载!=运算符。
是否成员成员函数:
像会具体改变左侧对象的值的情况下,作为成员函数的。->,[ ],(),=等应该作为成员函数重载的,而复合运算符不一定一定是成员函数。而具有对称性的运算符,即如关系,相等性,算术运算和位运算等,一般不为成员函数。或者需要其左右侧类对象类型不同,可交换位置,通常设置为非成员函数。
重载io(<<和>>)运算符:
重载<<运算符:
一般用于输出,即第一个参数为相应的非常量ostream对象的引用,非常量是因为其向流输入内容会改变状态,而引用则是ostream不可复制(第13章的禁止拷贝复制,一个流不能被复制),而第二个参数为其复制对象常量的引用,输出不改变其内容,引用节省空间。为了后续的运算(连续<<运算),返回类型为其ostream的引用。
格式 ostream& operator <<(ostream&,const data&);
最好不要在重载<<运算符函数体内容添加格式控制的语句,类似换行符,只要负者输出内容而不负责控制格式。这样就不能实现将后续内容放在同一行,也不能为成员函数,因为其左侧对象要为ostream,而如果为成员函数,其左侧绑定为当前类实例化对象(this)。因此要访问类对象,就定义为友元函数即可。
重载>>运算符:
第一个参数为要读取流的引用,第二个参数为将要读取到的数据的非常量(因为要读入数据,会改变)引用,返回相应流的引用。
istream&operator>>(operator&,data&);
与<<运算符不同,>>运算符重载时必须能够处理输入失败的情况。例如读入错误的数据类型,当与输入要求的类型不一致时,会发生错误,从而导致后续的输入无法正常运行。当读到文件末尾或者其他问题的发生错误。我们要负者处理错误,或是将其要保存的数据初始化。最好向io库报告错误。
算术运算符:
可以支持左右对象的交换,不为类成员函数,一般不需要改变变量的状态,故参数类型为常量引用。而它的计算结果为一个临时量,如果一个类要定义重载算术运算符,一般要先定义其复合赋值运算符,用复合赋值运算符来实现算术运算符的定义。
例:
而当类需要比较相等时,最好定义重载==运算符,这样方便记忆的同时也十分快捷,同时要定义其对应的!=运算符,并且其中一个运算符要用另一个运算符来简化定义。
关系运算符:
相关的容器(vector。。。)会要求所放入的元素支持一定的关系运算(<运算符),即比较大小。但要注意,如果一个类同时定义了<运算符和==运算符,那么不仅是要满足<比较,还要满足其==配套的!=时,如果两个类实例!=,则其应该是具有<关系的。这并不是都能满足的,因为<是相对于其中一个数据进行比较的,而当!=符号是对于多个数据进行判等的,有时会导致其中有的数据不一样但是对于比较的数据是相同的,所以这种情况下最好不要定义<运算符。(例如一个图书类,有书名号和价格,比较的时候是比较价格大小排序,但是!=是比较书名号和价格,而在书名号不同但是价格相同时,<运算符没用了)
赋值运算符(要定义为成员函数):
像之前的拷贝复制运算符和移动赋值运算符(13章),还可以定义其他的赋值运算符,类似vector标准库还定义了第三种赋值,以列表形式进行赋值。
而像复合赋值运算符,虽然可以不定义为成员函数,但最好都定义为成员函数。并且返回其引用。
下标运算符(必须为成员函数):
类似于内置数据类型的数组,【】返回的是类中特定顺序的元素,与内置类型也一样,返回的是元素引用,保障其能放在表达式的左右, 而且要定义常量版本和非常量版本的[]运算符。保证在常量对象使用下标时不可改变其元素。
前置和后置递减递加运算符:
前置递增递减运算符(基本为成员函数):
需要先检查其移动是否合法,有无超出范围。而前置是返回的是递增递减后元素的引用。
后置递增递减运算符:
后置与前置不同,不会返回递增后元素的引用,而是返回一个临时量用于保存未移动前的数据。而二者重载区分的标志为后置的参数列表中会多一个int型的参数(无需为其命名)。编译器会为其提供一个实参为0的数。
而当我们要显式用函数的形式调用后置运算符时,必须传给它一个int实参,来告诉编译器我们调用的是后置运算符。
成员访问运算符(通常用于智能指针或是一些有指针的数据成员时):
重载*和->为访问类的成员的运算符,而->一定要是成员函数,而*通常是成员函数。
而我们可以将他定义为常量版本的,适用于常量的实例化对象,因为我们只是得到其引用或是指针,并不会改变他的内部数据的状态。(而如果*或是->得到其内部数据要改变时,也会因为是指针形式的,如果常量类型则只是其指针所绑定的地址不能更改,而可以改变其指针所绑定的值)
箭头运算符的限定:
而我们可以将*重载运算符定义为任何我们想要的操作,返回的类型可以任意,但是->运算符不行,他一定要是成员访问的作用。所以重载的->运算符一定要是返回一个指针或者是重载了->的类,其他会报错。
函数调用运算符(必须为成员函数)函数对象和lambda表达式:
一个类可以定义函数调用运算符,使其行为像函数一样,但比函数更加灵活(还可以存储状态)。
可以定义多个函数调用运算符,要其参数数量和类型不一致。而这种类定义了函数调用运算符也被称作函数对象。同时也能定义数据成员。实现定制相应不同的操作。其可以定义0或者多个形参。
而函数对象常常用于泛型算法的实参
我们可以向标准库中的算法传递任何类别的可调用对象(只要一个对象或者一个表达式,只要能对其使用调用运算符则其就是可调用的,像:函数,函数指针,lambda表达式,重载了调用运算符的类),在其作为参数时,会自动调用其调用运算符进行传参。
lambda是函数对象:
lambda表达式实质就是被编译器翻译成一个未命名的类的未命名对象,该类中有一个重载的函数调用运算符。
stable_sort(words.begin(),words.end(),[](const string &a,const string &b){return a.size()<b.size();});
其行为类似于下面:
class ShortString{
public:
bool operator()(const string &s1,const string &s2)const
{return s1.size()<s2.size();
}
};
其产生的类只有一个函数调用运算符成员,而且不含有默认构造函数,赋值运算符及默认析构函数,是否含有默认的拷贝/移动构造函数视捕获的数据成员类型而定(在13章有提到默认拷贝和移动的规则)。且默认情况下lambda不能改变它捕获的变量。因此在lambda产生的类中函数调用运算符是一个const成员函数。如果为可变的,则调用运算符就不是const。
等价于:
stable_sort(words.begin(),words.end(),ShorterString());
而当lambda捕获函数外的变量时:
当一个函数lambda表达式通过引用来捕获变量,由程序负责确保lambda执行时引用的对象确实存在。因此编译器可以直接用该引用而无须在lambda产生的类中将其存储为数据成员。
而当通过值捕获的方式拷贝到lambda中,会被值捕获的变量拷贝到lambda。这时lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。将其捕获的变量的值来初始化数据成员。
这个合成的类不含有默认构造函数,因此必须提供一个实参初始化。
auto wc = find_if(words.begin(),words.end(),SizeComp(sz));
lambda是通过匿名的函数对象来实现的,是其函数对象在使用方式上的简化。当代码需要一个简单的函数且并不会在其他地方被使用时,就可以使用lambda表达式。而如果需要多次使用,并且需要保存某些状态的话,使用函数对象会更加合适。
后续继续补充。。。(明天)