1. 基本概念
函数名格式为 operator+要定义的运算符号
除了重载的函数调用运算符operator() 外,其他重载运算符不能含有默认实参。
一般情况下重载的运算符函数参数数量和运算符作用的运算对象数量一样多,且按顺序一一对应。
重载运算符或者是类的成员函数(其实也就保证了一个类类型的参数),或者至少含有一个类类型的参数。也就是说无法重载作用于内置类型的运算符。
可以重载大部分运算符,且只能重载已经存在的运算符,重载的运算符的参数数量,优先级,结合律都和原运算符一样: 不应该重载有求值顺序/短路求值特性的运算符,如&& ,|| 等等。以及不应该重载已经内置定义了对类作用含义的运算符,如& 和, ,二者已经有了内置对类的含义,不应重载。 重载运算符时,最好将重载函数定义为与内置类型一致的含义。
1.1 是否定义为成员函数
当把运算符重载为成员函数时,运算符左侧对象必须是类类型对象。假设类string以成员函数方式重载了加法(事实上是以非成员函数重载的),s是string的一个对象,那么: 上述加法等价于s.operator+("!") ,string有一个非explicit的构造函数,可以将const char*参数转化为string,因此是正确的。 而: "hi"没有名为operator+ 的成员函数,所以是错误的。
实际上,string将加法定义为普通的非成员函数:
2. 输入输出运算符
2.1 重载输出运算符<<
输出运算符第一个形参是非常量(需要写输出流)ostream的引用(ostream对象不能拷贝)。第二个形参是要打印的内容,通常是常量引用。
鉴于输出运算符左侧对象是ostream对象,所以<<必须定义为非成员函数。
2.2 重载输入运算符>>
输出运算符第一个形参是istream的引用(istream对象不能拷贝)。第二个形参是要读入的目的,通常是非常量引用。
3. 算术和关系运算符
3.1 相等运算符
定义相等运算符== 时,一般也会定义不等运算符!= 。
和上述的算术运算符和复合赋值运算符一样,相等运算符和不相等运算符一个应该把工作托付给另一个。即定义一个,另一个调用它完成功能。
3.2 关系运算符
4. 赋值运算符
赋值运算符必须定义为成员函数。
前面介绍的拷贝赋值运算符和移动赋值运算符都是在同类型的对象直接的赋值。除此以外,还可以定义将不同类型的对象赋予本类型对象的赋值运算符。
5. 下标运算符
下标运算符必须定义为成员函数。
返回所访问元素的引用(这样做的话下标可以出现在等式的任意一端)。
示例如下: 可以看出,除了返回类型不同外,函数接收的参数也不同,返回const引用的下标运算符接收一个常量对象,而返回普通引用的下标运算符接收一个普通对象。
6. 递增和递减运算符
前置版本的递增/递减运算符通常返回递增/递减后对象的引用。
为了区分前置和后置,引入额外形参。后置版本接受一个额外的不被使用的int形参,编译器为这个形参提供值为0的实参。 后置版本先要保存对象的状态。最后返回的也是改变前的值。
同样的,后置版本可以调用前置版本完成自己的功能。
可以显式调用后置版本,这种情况下就必须要自己传递额外参数完成函数重载了。在用++/-- 时编译器会帮忙干这些事。
7. 成员访问运算符
在重载箭头运算符时,必须保留成员访问这个基本含义,不能像其他运算符重载时那般自由。
8. 函数调用运算符
使用示例: 这样的类和普通函数的区别在于他是一个"类",也就是有各种各样的成员用于存储状态。如下例,存储了所要写的流和分隔符:
8.1 lambda是函数对象
使用lambda时,编译器会为其产生一个未命名类的未命名对象。该类中有一个重载的函数调用符。 如果捕获列表内存在需要捕获的变量: 1.如果是引用捕获,无需在lambda产生的类中存储,由编译器保证所引用的对象在引用时仍然存在。 2.如果是值捕获。则需要为捕获的变量建立数据成员,并构建构造函数初始化这些成员。例:
lambda表达式如下: 对应的类如下:
8.2 标准库定义的函数对象
标准库定义了一组表示算术运算、关系运算、逻辑运算的类,这些类被定义为模板形式,可以为其指定具体的作用类型,这些类通过重载函数调用运算符,生成可以替换运算符的函数对象。
如plus类表示加法,可以用plus < string >定义对string的加法函数调用,用plus< int >定义对int的加法。 使用示例: 下列是标准库定义的函数对象类型,这些类型都定义在functional 头文件内: 比较无关指针会产生未定义的行为,但是可以使用标准库提供的函数对象来完成比较:
8.3 可调用对象与function
调用形式指明了调用返回的类型以及传递给调用的实参类型。
可调用对象(泛型算法章节)也有类型,lambda 创建的对象有未命名的类类型,函数和函数指针的类型由返回值类型和实参类型共同决定。
不同类型的可调用对象可能具有相同的调用形式。如下: 面对这种情况,需要一个函数表,存储指向这些可调用对象的指针。
但是如何构建函数表也是个问题,一般而言,这个函数表可用类似C++中图的数据结构实现,但是这种方式要求值类型相同,而上述的三个可调用对象都有自己的类型,所以不行。
可以使用一个名为function 的标准库类型解决上述问题,fuction是一个模板,需要指定本function种存储的可调用对象的调用形式,function 定义在头文件functional 内。 function的使用: function可以解决上述的问题: 函数表的应用: 不要将函数名字放如function对象内(可能会重名),应该将函数指针放进去,或者使用lambda。
9. 重载、类型转换与运算符
9.1 类类型转换(用户定义的类型转换)
转换构造函数(其他类型->类类型)和类型转换运算符(类类型->其他类型)构成了类类型转换。
类型转换函数是一种特殊的成员函数,形式如下: type可以是任何类型,只要该类型能作为返回值。(数组和函数不可作为返回值,但指针和函数指针可以)
类型转换运算符没有显式返回类型,没有形参,必须定义为类的成员函数。且类型转换一般不需改变待转换对象内容,所以一般被定义为const。
例: 第一条语句:执行一个赋值,调用SmallInt的 operator= 成员函数,将3.14作为参数传给operator= ,参数拷贝时需要调用构造函数(是拷贝还是移动还是普通构造函数取决于参数类型),而构造函数参数是int类型,所以使用内置类型转换完成double到int的转换。
第二条语句,需要将si参与运算,涉及SmallInt的类型转换,会调用类型转换函数,将int类型的返回值val替换si,然后是int与double的运算,由内置类型完成后续转换。
在大多数情况下,类很少提供类型转换,因为当这些转换隐式发生时,会产生意外的结果,假设istream定义了向bool类型的转换时: 本来cin对象没有定义<<运算符,但是在这种情况下会隐式进行类类型转换,变成bool,编译会通过。
为了防止这样的隐式转换发生,类似于构造函数,引入了显式的类型转换运算符,就在转换语句前加上一个explicit。 使用时: 即使定义了显式类型转换,在如下情况下,类型转换还是会隐式执行: 有了显式类型转换,就可以处理上述问题了,将由istream向Bool的类型转换定义为显式类型转换。
9.2 避免二义性类型转换
必须保证类类型和目标类型间只存在一种转换方式,否则,代码将具有二义性。
产生多重转换路径的情况:
9.2.1 函数重载与二义性转换
调用重载函数时,如果多个用户定义的类型转换都提供了可行匹配,则认为这些类型一样好,忽略可能出现的标准类型的转换。只有当可行函数能通过同一个类型转换函数得到匹配时,才会考虑标准类型转换。注意和前面单个函数的匹配规则区分,单个函数是要考虑标准类型转换级别的。
9.3 函数匹配与重载运算符
使用函数时,具有该名字的成员函数和非成员函数不会彼此重载,调用函数的语法形式能区分二者,运算符不能,所以当使用重载运算符时,重载的候选集要比函数重载大得多。
|