第十四章 重载运算与类型转换

版权声明:转载请注明出处 https://blog.csdn.net/weixin_39918693/article/details/86570324


C++语言本身定义了大量的运算符以及内置类型的自动转换规则

当运算符被用于类类型对象时,C++语言允许我们为其指定新的含义。同时,我们也能自定义类类型之间的转换规则(隐式的)

明智的使用运算符重载能令我们的程序更易于编写和阅读


一、基本概念

重载的运算符是具有特殊名字的函数,他们的名字由关键字operator和其后要定义的运算符号共同组成,其他结构与普通函数一样

重载运算符函数的参数数量与该运算符作用的运算对象的数量一样多。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参

如果一个运算符函数是成员函数,则它的左侧运算符对象绑定到隐式的this指针上,因此,成员运算符函数的显式参数数量比运算符的运算对象总数少一个

对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数

我们不能重载运算符作用于内置类型对象时的含义

对于一个重载的运算符来说,其优先级和结合律(不是求值顺序)与对应的内置运算符保持一致

扫描二维码关注公众号,回复: 5006570 查看本文章

重载运算符的调用形式:两种

&&、||、,运算符不应该被重载,应为我们无法保留这些运算符的求值特性(特定的求值顺序和短路求值属性)

C++语言已经定义了逗号运算符和取地址运算符作用于类类型对象时的特殊含义。所以这两者不应该被重载

重载运算符应该保持与内置类型一致的含义,尽量明智的使用运算符重载

如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符

  • 1、赋值、下标、调用和成员访问箭头运算符必须是成员
  • 2、复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
  • 3、改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
  • 4、具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此他们通常应该是普通的非成员函数

程序员希望能在含有混合类型的表达式中使用对称性运算符。如果我们像提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。彻底理解见p493


二、输入和输出运算符

重载输出运算符<<,注意两个参数的类型,理解

输出运算符尽量减少格式化操作,使用户有权控制输出细节

重载的输入输出运算符函数必须是非成员函数,而且一般被声明为友元。原因你懂的

重载输入运算符,注意第二个参数的形式。输入运算符必须处理输入可能失败的情况,而输出运算符不需要

我们一般不逐个检查每个读取操作,而是等读取了所有数据后赶在使用这些数据前一次性检查

当读取操作发生错误时,输入运算符应该负责从错误中恢复(避免出现误导性后果)

标示错误:通常情况下,输入运算符只设置failbit。除此之外,设置eofbit表示文件耗尽,而设置badbit表示流被破坏。最好的方式是由IO标准库自己来标示这些错误

我们平时用的那种直接键入数据的方式没有文件末尾


三、算术和关系运算符

通常情况下,我们把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。因为这些运算符一般不需要改变运算对象的状态,所以形参都是对常量的引用。(类型转换与改变运算对象的状态并不等价)

算术运算符通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,其常常位于一个局部变量之内,操作完成后返回该局部变量的副本作为其结果。如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。此时,最有效的方式是使用复合赋值来定义算术运算符

如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使得用户更容易使用标准库算法来处理这个类

定义相等性运算符的类通常也定义了关系运算符。因为关联容器和一些算法要用到小于运算符,所以定义operator<会比较有用

对于类类型来说,如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类同时还包含==,则当且仅当<的定义和==产生的结果一致时才定义<运算符。不太懂


四、赋值运算符

赋值运算符必须先释放当前内存空间,再创建一片新空间,要根据实际情况决定是否需要考虑自赋值的情况

赋值运算符函数必须为成员函数

赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这样做。这两类运算符都应该返回左侧运算对象的引用


五、下标运算符

下标运算符必须是成员函数

我们最好同时定义下标运算符的常量版本和非常量版本,你懂的

如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用

具体格式见p501,把指向数组首元素的指针当成数组名使用??????????????


六、递增和递减运算符

定义递增和递减运算符的类应该同时定义前置版本和后置版本。这些运算符通常应该被定义为类的成员

为了区别前置和后置运算符,后置版本接受一个额外的(不被使用的)int类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参。该参数只是用于区分作用,我们不会用到int形参,所以无需为其命名

为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用

用前置实现后置

如果是显式的调用后置运算符,则需要为其提供一个实参


七、成员访问运算符

解引用运算符和箭头运算符

箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此

对于重载箭头运算符,永远不能丢掉成员访问这个最基本的含义。我们对箭头运算符的重载可以改变的是箭头从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变

箭头运算符的运算对象必须是指向类对象的指针或者是一个重载了箭头运算符的类的对象

注意箭头运算符的执行流程

这两个运算符一般都重载于伴随指针类

重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象


八、函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比他们更加灵活

我们使用调用运算符的方式是令一个重载了调用运算符类的对象作用于一个实参列表。调用对象实际上是在运行重载的调用运算符

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别

如果一个类定义了调用运算符,则该类的对象称作函数对象,该类称作函数对象类

函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符函数中的操作

函数对象常常作为泛型算法的实参

当我们编写了一个lambda后,编译器将该表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符

默认情况下lambda不能改变它捕获的变量。因此在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda被声明为可变的,则调用运算符就不是const的了。

其实lambda可变与否,mutable关键字只与值捕获有关。引用捕获是够可变,取决于被引用捕获的那个对象是否是const的

当一个lambda表达式通过引用捕获变量时,将由程序员负责确保lambda执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员

通过值捕获的变量被拷贝到lambda中。因此,这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员

lambda表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定????????????

标准库函数对象:标准库在functional头文件中定义了一组表示算术运算符、关系运算符和逻辑运算符的类(模板),每个类分别定义了一个执行命名操作的调用运算符。为模板指定的类型就是调用运算符的形参类型

标准库规定其函数对象对于指针同样适用。我们之前曾经介绍过比较两个无关指针将产生未定义的行为

关联容器使用less<key_type>对元素排序,因此我们可以定义一个指针的set或者在map中使用指针作为关键值而无需直接声明less

C++语言中有如下几种可调用对象:函数、函数指针、lambda表达式、bind创建的对象、重载了函数调用运算符的类

可调用对象具有类型。lambda有它自己唯一的类类型,函数及函数指针的类型则由其返回值类型和实参类型决定

两个不同类型的可调用对象却可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型

函数表p511


九、重载、类型转换与运算符

转换构造函数和类型转换运算符共同定义了类类型转换,这样的转换有时也被称作用户定义的类型转换

类型转换运算符是类的一种特殊的成员函数,它负责将一个类类型的值转换为其他类型
operator type() const

其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为作为函数的返回类型。因此,我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型

类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成const成员

尽管编译器一次只能执行一个用户定义的类型转换,但隐式的用户定义类型转换可以置于一个标准(内置)类型转换之前或之后,并与其一起使用

因为类型转换运算符是隐式执行的,所以无法给这些函数传递实参,当然也就不能在类型转换运算符的定义中使用任何形参。同时,尽管类型转换函数不负责指定返回类型,但实际上每个类型转换运算符都会返回一个对应类型的值

明智的使用类型转换运算符能极大的简化类设计者的工作

在实践中,类很少提供类型转换运算符。但定义向bool的类型转换还是比较普通的现象

编译器通常不会将一个显式的类型转换运算符用于隐式类型转换

当类型转换运算符是显式的时,我们也能执行类型转化,不过必须通过显式的强制类型转换才可以

在一些地方,显式的类型转换可以被隐式的执行:if、while及do语句的条件部分;for语句头的条件表达式;!、||、&&的运算对象;条件运算符的条件表达式

转换为bool

如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则的话,我们编写的代码将很可能会具有二义性

提供多重转换路径的情况:两个类提供相同的类型转换;类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系到一起

通常情况下,不要为类定义相同的类型转换;对于一个给定的类来说,最好只定义最多一个与算术类型有关的类型转换

我们无法使用强制类型转换来解决二义性问题,因为强制类型转换本身也面临二义性问题(可以用显式指定转换方式来解决该问题)

如果类定义了一组类型转换,他们的转换源(或者转换目标)类型本身可以通过其他类型转换联系到一起,则会产生二义性问题

当我们使用用户定义的类型转换时,如果转换过程包含标准类型转换,则标准类型转换的级别将决定编译器选择最佳匹配的过程

要想正确的设计类的重载运算符、转换构造函数及类型转换运算符,必须加倍小心。尤其是当类同时定义了类型转换运算符及重载运算符时特别容易产生二义性

当你已经定义了一个转换成算术类型的类型转换时,就不要再定义接受算术类型的重载运算符了(使用内置版本就好了,原因你懂的)

不要定义转换到多种算术类型的类型转换。让标准类型转换完成向其他算术类型的转换工作

总而言之,除了显式的向bool类型的转换之外,我们应该尽量避免定义类型转换运算符函数并尽可能的限制那些“显然正确”的非显式构造函数

如果在调用重载函数时我们需要使用构造函数或者强制类型转换来改变实参的类型,则这通常意味着程序设计存在不足

重载函数与用户定义的类型转换:标准类型转换不影响匹配过程

在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性

重载的运算符也是重载的函数。因此,通用的函数匹配规则同样适用于判断在给定的表达式中到底应该使用内置运算符还是重载的运算符

当运算符出现在表达式中时,候选函数集的规模要比我们使用调用运算符调用函数时更大

运算符出现在表达式中的情况对应两种掉用形式

和普通函数调用不同,我们不能通过调用的形式来区分当前调用的是成员函数还是非成员函数

  • 1、当我们使用重载运算符作用于类类型的运算对象时,候选函数中包含该运算符的普通非成员版本和内置版本。除此之外,如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本也包含在候选函数中
  • 2、当我们调用一个命名的函数时,具有该名字的成员函数和非成员函数不会彼此重载,这是因为我们用来调用命名函数的语法形式对于成员函数和非成员函数来说是不相同的
  • 3、当我们通过类类型的对象(或者该对象的指针及引用)进行函数调用时,只考虑该类的成员函数
  • 4、当我们在表达式中使用重载的运算符时,无法判断正在使用的是成员函数还是非成员函数,因此二者都应该在考虑的范围内

表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数(包括内置)

如果我们对同一个类既提供了转换目标为算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/86570324