目录
成员访问运算符(通常用于智能指针或是一些有指针的数据成员时):
函数调用运算符(必须为成员函数)函数对象和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表达式。而如果需要多次使用,并且需要保存某些状态的话,使用函数对象会更加合适。
标准库定义的函数对象(c++11标准的function函数):
标准库还定义了一组表示算法,逻辑,关系运算符的类,且定义了一个执行命令的调用运算符。
都被定义为了模板的类型,可以指定具体的应用类型。通常这些被用在标准库算法中用来代替那些默认的运算符。
而且标准库定义的函数对象对于指针同样适用。如两个无关指针进行比较会产生未定义1行为,而我们可以通过比较指针的内存来sort其在vector的位置。直接这样会产生未定义的行为,但是可以用标准库函数对象来实现这个。
可调用对象与funtion:
几种可调用对象都有他对对应的类型,像lambda有对应的未命名的类的类型,而函数和函数指针则是根据他返回类型和参数来区分类型。但是两个不同类型的可调用对象却可能同时共享一种调用形式。调用类型指名了调用返回的类型和传递给调用的实参类型。而不同的调用对象类型有时可对应同一种调用形式。
虽然上述类型不同,但是他们都是共享同一种调用形式。
而我们可以定义一个函数表用于存储指向这些可调用对象的“指针”,当需要什么函数时,从表中找到。可以通过map来实现,用运算符的符号string为关键字,而其函数为值。
例:
而lambda不是函数,而是一个类类型。但我们可以使用function新标准库来解决这个问题。
标准库function为一个模板,必须提供额外的信息即为该function类型能够表示的对象的调用形式。function<int (int,int)>表示接受两个int实参,返回int的可调用对象。
这样就可以添加所有为此调用形式的调用对象了,
而function是无法区分重载函数的,会有二义性。故不能直接将函数名作为参数,而是要存储函数指针。
重载,类型转换与运算符:
类具有隐式将实参类型转换成类类型的隐式转换,要求其构造函数只接受一个实参,即建立了这个实参向类转换的方式。这种转换可以用于直接初始化和拷贝初始化中,一定要是初始化,且只允许一次的隐式类型转换。但是我们是可以在一次内置类型的转换中添加一次用户自定义的类型转换。
class S
{
int a;
public:
S(int b):a(n){}
}
S x = 2;//由int转换成S,在赋值给x
S x =2.4;//此时double先内置类型转换成int,后面可以衔接用户自定义的类型变换。double->int->S再向x赋值。
类类型转换运算符:
而我们还定义了由类类型向其他类型转换的方式,即为重载类型转换运算符:
operator type()const;
该类型转换运算符没有返回类型(或者隐式的定义了返回类型为要转换的type),没有形参,一定为成员函数,type是一种类型(即类转换成的类型),类型转换运算符可以面向任何的可以作为函数返回值的类型进行定义。不能转换成数组或者函数类型,但可以转换成指针或引用类型。
避免滥用,只有在一一对应的关系下使用才不会歧义。
类型转换符可能会带来意外的结果:
大部分情况下的类向其他类型的转换可能不会让人理解,通常都是将其类转换成bool类型用于判断。但是在早期的c++中,因为bool类型是算术类型,故可以被用到任何算术的表达式中进行转换。特别是当istream有向bool转换的方式时,以下的表达式就是将被允许.
int a=27;
cin<<a;
虽然cin没有定义<<函数,但是有向bool类型转换的隐式方式,所以会将cin转换成bool并将其向左移位27位,显然是错误的用法,但是允许编译通过。
显式的类型转换运算符(c++11explict):
为了避免这种用法,我们可以不允许隐式的类型转换,即像在构造函数中添加关键字一样,加上explict限定字。
单与explict构造函数不同,在一些情况下会这种显式的类型转换会被隐式调用。就是在用作条件时,即ifelse,for的条件,do条件部分,,while,?:,逻辑非,逻辑或,逻辑与。
举例(istream)类类型转换为bool:
当我们使用流对象时,流对象本身就定义了operator bool,用于转换成bool类型。
while(cin>>a);(先读入数据到a,然后对cin进行隐式(或者是显式转换的隐式使用,用于判断)转换成bool,如果cin的状态是good,则判断返回真,否则为假。而又因为基本都用于判断(会让explict定义的显式转换来隐式调用),所以基本用关键字explict来限定。
避免二义性的类型转换:
我们可以定义多个类向不同类型的转换,但是必须保证类转换到目标类型只存在唯一的一种转换方式。有两种情况:1.这两种类提供了相同的类型转换,类A提供向类B转换的途径,而类B也提供转换类A的途径。2.一个类定义了多种转换规则,而这些涉及的类型可以通过其他类型转换联系到一起,类似于算术运算符。所以最好只定义一个转换成算术运算符的规则。
解决这种错误可以显示说明调用的是那个类来使用构造函数或是类型转换运算符,但我们不能通过强制类型转换来解决,因为其本身也面临着二义性。
还有类的转换源或者转换对象可以相互转换,也会产生二义性的问题。 原因是上述的f2和a2他们的标准类型转换的级别相同。
而当我们使用自己的类型转换时包括标准类型的转换,则标准类型转换将决定编译器选择最优的。
当我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换来决定最佳匹配是哪个。
重载函数与转换构造函数:
如果一个重载函数定义了不同类型的形参类型,而这些类型都定义了同样的转换构造函数,会产生二义性。
struct C{
C(int);
};
struct D{
D(int);
};
void manip(const C&);
void manip(const D&);
manip(10);
这时有二义性,10都能转换成C或者D,没有优先级区分,编译器不知道优先调用哪一个。而我们可以用显式构造来消除二义性:
manip(C(10));
重载函数与用户定义的类型转换:
如果调用重载函数时,两个用户定义的类型转换都提供了可行的匹配时,则我们会认为这些类型转换一样好。不会考虑任何可能出现的标准类型转换的级别。即使其中一个不用类型转换能够精确的匹配。
struct{
C(int);
};
struct D{
D(int);
};
struct E{
E(double);
};
void manip(const C&);
//void manip(const D&);
void manip(const E&);
manip(10);//二义性错误,此时E和C的转换构造函数都同等于作用,无关标准类型转换,即double转成int这一过程,会同时考虑manop(c(10))还是manip(E(double(10))
只有当重载函数是对于同一个类定义的类型转换函数匹配时,才会考虑到其中出现的标准类型转换。
函数匹配与重载运算符:
重载的运算符也是重载函数,因此同时适用于函数的匹配规则。但是当运算符函数出现在表达式中时,候选函数集的规模要比我们使用调用运算符调用函数时更大。
a sym b
a.operatorsym(b);//a有一个operatorsym的成员函数
operatorsym(a,b);//普通函数
我们不能通过调用的形式来区分当前调用的时成员函数还是非成员函数。当我们使用重载运算符作用于类类型对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象也是类类型,则定义在其中的运算符的重载版本也在候选函数中。
这是因为当我们调用一个正常的命名的函数时,有该名字的成员函数和非成员函数不会彼此重载。我们对于调用命名函数的语法对于成员函数和非成员函数是不同的。当通过类类型的对象(或该对象的引用和指针时)进行函数调用指只会考虑其成员函数。而当我们在表达式中使用重载运算符时,则没有区分的语法来判断正在使用的时成员函数还是非成员函数,二者都要考虑在内。
class S{
int display();
int operator +(S &);
};
daiplay(S &);
int main(){
S p1,p2;
display(p1);//明显的正常函数调用,不会考虑到类中的函数
p1.display();//表明是类成员函数调用,只会考虑到类成员函数
p1+p2;//无法区分
return 0;
}
举例:
class S{
friend S operator +(const S&,const S&);
public:
S(int=0); //转换源是int的类型转换
operator int() const//转换目标为int的类型转换
{return val;}
private:
std::size_t val;
S s1,s1;
S s3= s1+s2; //使用重载的operator+
int i = s3+0;//二义性错误,可以将s3转换成int,执行加法运算,或者将0转换成S,使用S类中的+;
如果我们对同一个类即提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则会遇到重载运算符与内置运算符的二义性问题。