专栏首页未竟东方白【笔记】《C++Primer》—— 第14章:重载运算和类型转换

【笔记】《C++Primer》—— 第14章:重载运算和类型转换

这一章介绍了对运算符的重载和类型转换,其中最重要的是对各种运算符的运用,14.8对function类的运用和14.9对类型转换时可能产生的二义性的理解,其余的内容不多,这篇看起来很多节但其实只是因为内容比较散而已。

14.1 基本概念

  • 运算符重载在13章中提到过一点点,重载的运算符实际上是具有特殊名字能被特殊调用的函数
  • 重载运算符的名字由 operator运算符 组成,和普通的函数一样,重载运算符函数也需要自己的返回值,参数和函数体
  • 重载运算符的参数数量必须和这个运算符默认情况下的参数一致,而且其优先级和结合律无法改变即与默认情况一致
  • 有的运算符(如加号+)有一元版本和二元版本,我们用参数数量区分它们
  • 我们不能创建新的运算符,且我们只能重载下表中的一些运算符
  • 我们可以像普通的运算符一样使用重载的运算符,也可以像普通的函数一样使用重载运算符函数,当这个函数是成员函数时,我们也可以像使用普通的成员函数一样使用它
  • 重载运算符的本质是函数调用,因此向逻辑运算符,逗号运算符之类的运算符内在的短路求值特性等将被舍弃,参数们都会被以未定义的顺序求值
  • 尽管我们可以重载大多数的运算符,但是对于逗号运算符,取地址运算符和逻辑运算符我们一般不去重载它因为容易起误会
  • 我们应只在操作含义清晰明了时才重载运算符,且有些运算符我们最好还限定其成员性:
    • 赋值,下标,调用,箭头 应该是成员
    • 复合赋值 一般是成员
    • 递增递减等会改变对象本身的应该是成员
    • 参数两端可以改变顺序的如算数,相等,关系,位运算 一般不是成员
  • 如果想要定义含有类对象的混合类型表达式,注意一定要是非成员,具体的理解可以试着把运算符写回函数调用形式来判断是否适合写为某个类的成员函数

14.2 输入和输出运算符

  • 输出运算符的第一个参数是对一个非常量的ostream的引用,第二个参数是我们想要打印的目标类型的引用,返回它的ostream形参引用
  • 一点规范:输出运算符应该主要负责打印对象的内容而非控制格式
  • 输入运算符的第一个参数是对一个非常量的istream的引用,第二个参数是我们想要读取的目标类型的引用,返回它的istream形参引用
  • 一点规范:输入运算符必须处理可能失败的情况,生成符合规范的元素输入或其他方法,要尽可能保持流的正常工作且负责让流从错误中恢复

14.3 算术和关系运算符

  • 如果定义了算术运算符,一般也会定义复合赋值运算符,此时一般使用复合赋值运算符来定义普通的算术运算符(也就是让普通的算术运算符通过包装复合赋值运算符来实现)
  • 相等运算符的一般语义是比较类的每一个数据成员的值,当全部相等时才标识相等
  • 如果类有判断两个对象是否相等的操作,最好就是重载出相等运算符减少学习成本
  • 如果定义了相等运算符,一般也要能够判断一组对象中是否有重复的数据
  • 相等运算符应保证有传递性
  • 如果定义了==,一般也要定义!=,而且为了保证正确性,应该把其中一个的实现委托给另一个,通常先定义==再实现!=
  • 如果存在唯一可靠的一种<定义,那么就应定义<运算,且实现>,而且若同时包含了==的话,还应该保证<和==的结果一致时才定义<

14.4 赋值运算符

  • 之前有了移动构造和拷贝构造赋值运算符,但类还可以自定义赋值运算符,用于让其他类型作为右侧对象,返回一个当前类型的引用
  • 不论形参是什么,赋值运算符都必须定义为成员函数
  • 复合赋值运算符不是非得是成员函数,但是我们也倾向于定义为成员函数
  • 赋值运算符的参数一般都是const的

14.5 下标运算符

  • 下标运算符必须是成员函数
  • 我们一般会定义两个版本的下标运算符,一个返回普通引用,一个接受常量参数并返回常量引用
  • 常量版本是为了方便这个运算符用在常量函数中,确保作用在常量对象上时不会对这个对象进行赋值

14.6 递增和递减运算符

  • 递增递减的语义一般是让类在序列中移动,所以也建议是成员函数
  • 定义递增递减时应该同时定义好前置版本和后置版本,其中前置版本就是普通的定义方法,后置版本为了区分会附加一个int参数,这个参数一般不对其命名也不对其运算,其值是编译器自动传入的0
  • 为了和内置的运算符语义一致,后置版本应该返回原值
  • // 前置版本 FOO& operator++() { // 一般返回操作后自身的引用 /* do something... */ return *this; } // 后置版本,有一个空的int参数 FOO operator++(int) { // 一般返回的是操作前的原值 FOO temp = *this; /* do something... */ return temp; }
  • 当我们需要显式调用递增递减运算符时,我们需要对后置版本的参数传递一个任意int值,这样编译器才能区分出版本

14.7 成员访问运算符

  • 箭头运算符必须是成员函数,解引用运算符一般也是类的成员
  • 解引用运算符使用时应该检查目标是否在作用范围内
  • 箭头运算符一般不会有自己的操作,而是包装解引用运算符
  • 重载的箭头运算符有要求必须返回类的指针或者某个对象,但是解引用运算符没有要求

14.8 函数调用运算符

  • 重载函数调用运算符让我们可以像使用函数一样使用类的对象,这样的类由于能有自己的状态所以比普通的函数更加灵活
  • 调用运算符必须是成员函数,可以有多个参数不同的重载函数
  • 定义了调用运算符的对象称为函数对象,函数对象常常用作泛型算法的实参,我们之前使用的lambda表达式就是一种自动生成的函数对象
  • 默认情况下lambda表达式不能改变它捕获的变量因为它的函数调用运算符被重载为const的,如果把lambda声明为可变的,那么生成的调用符不是const了
  • lambda表达式产生的类没有默认构造函数,赋值运算符和析构函数,而是否有默认的拷贝/移动函数要视捕获的数据成员类型而定
  • 标准库functional中定义了一系列表示算术运算符,赋值运算符和默认析构函数的模板类,我们可以用这些类来实现简单的类计算或将其作为可调用对象传递到一些容器中替换掉默认的运算符改变容器的操作
  • 要注意这些标准函数对象对指针也是有效的,例如less可以比较指针间的内存地址
  • C++中有几种可调用对象:函数,函数指针,lambda,bind对象,函数对象。而可调用对象之间尽管可能类型不同但可能有相同的调用形式,例如都是接受两个int返回一个int的 int(int,int) 形式
  • 有时候我们想要有一个函数表储存指向可调用对象的指针,然后按照所需的调用形式来检索对象,对此我们可以用map来实现,让运算符为key,可调用对象为value
  • 但是此时会发现我们希望可调用对象为value,但不同的可调用对象有不同的类型,如函数指针与函数对象是不同的。标准库的functional针对这个问题定义了function类,function类接受一个可调用对象为模板,然后可以按需求返回其返回类型,参数类型等等信息,并且可以按照相同的方式调用这些不同类型的可调用对象
  • // 用std::function来统一不同类型的可调用对象 // 接受lambda std::function<int(int, int)> f1 = [](int i, int j) {return i + j; }; // 接受函数 std::function<int(int, int)> f2 = add; // 接受函数对象 std::function<int(int, int)> f3 = FOO();
  • 但是要注意我们不能将参数不同但名称相同的可调用对象之间存入function中,因为会产生二义性,解决方法是使用函数指针处理

14.9 重载,类型转换与运算符

  • 我们一样可以自定义所需的类型转换运算符,方法是一个没有返回类型也没有形参的必须定义为成员函数的operator type() const;函数。type是想要转换的类型,除了void外什么只要是可以作为返回值的类型就都可以。由于通常类型转换不会改变对象的内容所以一般都用const
  • 尽管编译器对一个对象一次只进行一次自定义的类型转换,但这个转换还可以放在一次内置转换之前或之后,也就是对象最多可能隐式发生两次转换,这就容易导致二义性问题
  • 如果一个类包含了以下几个情况,就很容易导致转换的二义性,本质上都是类与目标中存在多条路径:
    • 两个类提供了相互的类型转换,例如A.f()接受一个A的参数,然后此时有B,此时若B可类型转换为A,A也有一个由B为参数的构造函数,则会产生二义性。如果我们想要解决二义性可以显式写出转换函数来表明路径,注意我们无法用强制类型转换解决因为同样会产生二义性
    • 当某个类提供了多个向内置类型的转换时,可能会触发第二点所说的先转换为内置类型再发生内置转换,这也是二义性
    • 类似上面,自然也有当类有多个接受内置类型的构造函数时,可能触发不同路径下的内置类型构造
  • 当两个或以上的类型转换都提供了向两级以上的对同个目标的转换,则这些转换一样好,同样会有二义性。只有当所有相关的转换都请求了同一个转换时标准隐式转换的优先级才会有效
  • 如果调用重载时我们需要显式写出转换或用强制类型转换,则常常说明我们的设计有不足
  • 类型转换中有一个转换比较特别,那就是bool类型的转换。将类往bool类型进行转换是最常见的一种做法,但是由于转到bool类型很可能表达的并不是简单的数据语义,所以C11加入了显式类型转换来限制它,方法是在转换函数前加上explicit,这样编译器就不会隐式进行这个转换,也就是我们必须使用强制类型转换才能使用
  • 但是对于bool的特殊对待,在一些条件表达中,例如if,for,逻辑运算符和三目运算符中,bool转换会自动进行显式转换
  • 对类进行隐式类型转换最好尽量避免,因为语义常常比较模糊,建议除了bool类型外不要进行隐式的类类型转换
  • 和普通函数调用不同,我们不能通过调用的形式来区分当前调用的重载函数是成员函数还是非成员函数
  • 当我们使用重载的运算符时,编译器的候选函数集包括了同名的非成员函数和成员函数,也就是当我们在表达式中使用了目标是内置类型的类型转换时,需要考虑接下来要使用的运算符到底是内置的还是我们自定义的运算符,这也很容易引起二义性问题

本文分享自微信公众号 - 未竟东方白(WJDFB_),作者:读书中

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【笔记】《C++Primer》—— 第4章

    第四章的标题是表达式,主要讲的内容是平时在用的表达式中的运算符和类型转换等概念,内容不复杂但是却很基础很有用,很多平时习以为常的写法在这章才被系统解释了一次。不...

    ZifengHuang
  • 【笔记】《C++Primer》—— 第三部分:类设计者的工具

    这篇是第三部分的总结,基本上就是回看了之前的4篇笔记并且重新翻翻书梳理了一下,内容基本都是从前面的章节复制来的,长度较长,难度可能也比较大。

    ZifengHuang
  • 【Unity】瞎做个宝石迷阵吧!(2)——砖块交换

    我们要利用这几个变量来控制砖块的交换,先来做砖块的动画效果。在Bricks的Update里将代码改成这个样子,我们所需object在每帧要做出的变换我们...

    ZifengHuang
  • Python 运算符,你了解多少?

    ? 文 | 云豆 图 | 来源网络 ? 云豆贴心提醒,本文阅读时间6分钟,文末有秘密! 什么是运算符? 本章节主要说明Python的运算符。举个简单的...

    小小科
  • 「Java Grammar」:运算符

    计算机自打诞生以来,用作最多的就是进行计算,而计算离不开运算符,所以运算符在我们的Java语言中的地位举足轻重,我们现在就来了解一下Java给我们提供的运算符。

    山禾说
  • Python入门系列第二章--第四节:运算符(二)

    位运算符就是将数字转换为二进制进行计算,位运算符包括:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)、左移动(<<)、右移动(>>)。解释见下表:

    喵叔
  • 第二章--第三节:运算符(一)

    顾名思义,算术运算符就是用来进行算数运算的一些符号。那么算术运算符包含哪些呢?除了有我们常用的 加、减、乘、除 外,还有 取模、幂、取整除 ,这些运算符的解释详...

    喵叔
  • c++之运算符

    算数运算符:+(正) -(负) + - * / % i++(先赋值后自增) ++i(先自增后赋值) i--(先赋值后自减) --i(先自减后赋值)

    绝命生
  • Go语言学习01

    安包
  • C#运算符的优先级

    在C#中,一共有38个常用的运用符,根据它们所执行运算的特点和它们的优先级,为了便于记忆,我将它们归为七个等级:1、单元运算符和括号。2、常规算术运算符。3、位...

    郑小超.

扫码关注云+社区

领取腾讯云代金券