第十四章 重载运算与类型转换
14.1 基本概念
重载运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。
如果一个运算符函数是成员函数,则它的第一个运算对象绑定到隐式的this指针上
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。
1. 可以重载的运算符
通常情况下,不应该重载逗号,取地址,逻辑与和逻辑或运算符。
- 上述运算符的重载版本无法保存求值顺序和短路求值属性
- 逗号运算符和取地址运算符已经有了自己内置的含义。
2. 设计类时应该考虑的重载问题
3. 设计重载时成员函数和非成员函数的考虑
4. 非成员函数的对称性性质
当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象:
14.2 输入和输出运算符
1. 重载输出运算符 <<
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用。第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型
返回值一般要返回它的ostream形参
输入输出运算符必须是非成员函数
因为如果是成员函数,则第一个参数就必须是该类的一个对象,而这里第一个参数为ostream的一个引用。
可以将自定义输入输出运算符函数定义为非成员函数,然后再在相应的类里将其声明为友元
2. 重载输入运算符 >>
第一个形参是运算符将要读取的流的引用,第二个形参是将要督导的对象的引用。
返回值一般返回给某个给定流的引用
输入运算符必须处理输入可能失败的情况,而输出运算符不需要
如果错误发生前对象已经有一部分被改变,则应该将对象合理地置为合法状态
14.3 算数和关系运算符
通常情况下,把算数和关系运算符定义为非成员函数,这样能允许对左侧或右侧的运算对象进行转换。
这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
算数运算符通常会计算它的两个运算对象并得到一个新值,这个值常常位于局部变量之内,操作完成后应该返回该局部变量的拷贝
如果类同时定义了算数运算符和相关的复合赋值运算符(+=),通常情况下应该使用复合赋值运算符来实现算符运算符
1. 相等运算符
用来校验两个对象是否相等
如果一个类中含有判断两个对象是否相等的操作,则应该把该操作定义为 “==“而非其它的函数提供了”==“就意味着用户无需再费时费力地学习并记忆一个全新的函数名字
如果定义了==,则也应该定义”!=”
"==“和”!="中的一个应该把工作委托给另一个(像上面的例子一样,不等于判断委托给等于)
2. 关系运算符
关联容器和一些算法要用到小于运算符,所以经常需要定义operator<。
14.4 赋值运算符
赋值运算符都定义为成员函数,返回值为自身的引用
1. 列表赋值运算符
接受的参数为一个列表
2. 复合赋值运算符
复合赋值运算符不一定非得是类的成员,不过还是倾向于把包含复合赋值在内的所有赋值运算都定义在类的内部。
14.5 下标运算符
表示容器的类通常可以通过元素再容器中的位置访问元素,这些类一般会定义下标运算符operator[]
下标运算符必须是成员函数
定义下标运算符通常定义两个版本:
- 返回普通引用
- 返回常量引用
14.6 递增和递减运算符
在迭代器类中通常会实现递增运算符(++)和递减运算符(–),这两种运算符使得类可以在元素的序列中前后移动。
因为它们改变的是操作对象的状态,所以建议将其设定为成员函数。
定义递增和递减运算符的类应该同时定义前置版本和后置版本。
1. 定义前置递增/递减运算符
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用
2. 区分前置和后置运算符
后置运算符接收一个额外的int类型的形参用以区分,并且编译器为这个形参提供一个值为0的实参。(该值无需命名,不会用到)
3. 后置运算符返回的是一个值而非引用
后置运算符可以调用各自的前置版本来完成实际的工作
14.7 成员访问运算符
包括引用运算符(*)和箭头运算符(->)
箭头运算符和解引用运算符通常必须是类的成员
->返回指针 ,*返回引用
1. 对箭头运算符返回值的限定
对于形如point->mem的表达式来说,point必须是指向类对象的指针或者是个重载了operator->的类的对象。根据point类型的不同,point->mem分别等价于:
14.8 函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,但是相互之间应该在参数数量或类型上有所区别。
如果类定义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”。
函数对象常常作为泛型算法的实参
1. lambda是函数对象
当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符。
在默认情况下,由lambda产生的类中的函数调用运算符是一个const成员函数(不改变成员)。
lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数。它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。
表示lambda及相应捕获行为的类
当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。
通过值捕获的变量被拷贝到lambda中,由lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
值捕获的sz需要在类中以数据成员的形式存储
2. 标准库定义的函数对象
标准库定义了一组表示算数运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。
它们定义在头文件functional中
这些类都被定义成模板的形式,我们可以为其指定具体的引用类型,这里的类型即调用运算符的形参类型。
在泛型算法中使用标准库函数对象
标准库函数对象对指针同样适用
3. 可调用函数和function
可调用对象:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类。
可调用对象也有类型,两个不同类型的可调用对象可能共享同一种调用形式
标准库function类型
使用一个标准库类型function来解决多个调用对象共享同一调用形式的问题。
需要包括头文件functional
function是一个模板,创建时需要提供对象的调用形式信息
上图中的add、divide()、lambda表达式,如果不使用function类型,则不能三合一为"int(int, int)" 的函数类型
重载的函数与function
14.9 重载、类型转换与运算符
用户可以自定义类之间的类型转换
1. 类型转换运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其它类型。类型转换函数的一般形式如下所示:
类型转换运算符没有显式的返回类型,也没有形参,必须定义成类的成员函数。
类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员。
用户定义类型转换可以置于一个标准类型转换之后或之前
si+3.14,将si隐式转换为int,接着int再隐式转换为double。这里实现了两步转换
避免过度使用类型转换指针
显式的类型转换运算符
在转换运算符前面加上explicit修饰符
调用的时候需要显式转换
向bool的类型转换
2. 避免有二义性的类型转换
实参匹配和相同的类型转换
本例中A的构造函数合B的类型转换运算函数发生了二义性冲突
二义性与转换目标为内置类型的多重类型转换
A可以转换成int,也可以转换成double,在调用函数f2的时候,A到底是执行到int的转换还是到double的转换。A的两个转换运算符发生了冲突
lg是一个long类型,A的两个构造函数发生了冲突。
重载函数与转换构造函数
因为C和D的构造函数没有阻止隐式转换,所以10既可以转换为C也可以转换为D
重载函数与用户定义的类型转换
3. 函数匹配与重载运算符
重载的运算符也是重载的函数。通用的函数匹配规则也同样适用于判断在给定的表达式下应该使用内置运算符还是重载的运算符。