C++ Primer 0x0E 学习笔记

Posted 鱼竿钓鱼干

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ Primer 0x0E 学习笔记相关的知识,希望对你有一定的参考价值。

📔 C++ Primer 0x0E 学习笔记

更好的阅读体验(实时更新与修正)

推荐阅读 《C++ Primer 5th》知识点总结&练习题解

14.1 基本概念

  • 重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成,和其他函数一样有返回类型、参数列表以及函数体

  • 当一个重载的运算符是成员函数时,this绑定到左侧运算对象,成员函数的显式参数数量比运算对象的数量少一个

  • 对于一个运算符号来说,它或者是类的成员,或者至少包含一个类类型的参数,这意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义

  • 我们只能重载已有的运算符,无权发明新的运算符

  • 有的运算符既是一元运算符也是二元运算符,可以从参数数量推断是哪种

  • 对于一个重载的运算符来说,其优先级和结合律与对应的内置运算符保持一致

  • ::.*.? :不能被重载

  • 我们既可以直接将运算符作用于类型正确的实参间接调用data1+data2,也可以像普通函数那样直接调用operator+(data1,data2)

  • 某些运算符不应该被重载(逗号,取地址,逻辑与,逻辑或)

    • 某些运算符指定了运算对象的求值顺序,变成函数调用的话会失效。特别是逻辑与,逻辑或,逗号运算符。这些运算符的重载版本无法保留求值顺序和/或短路求值属性,因此不建议重载它们
    • 我们也不建议重载逗号运算符和取地址运算符,因为C++已经定义了它们作用于类类型对象时的特殊含义,如果重载会很不适应
  • 重载运算符使用与内置类型一致的含义,要清晰明了

    • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO一致
    • 如果类的某个操作检查相等性,则定义operator==operator!=
    • 如果类包含一个内在的单序比较,则定义operator<以及其他关系操作
    • 重载运算符的返回类型通常应该与内置版本返回类型兼容
    • 赋值运算符赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载赋值运算符应该继承而非违背内置版本的含义
    • 如果类含有算术运算符或者位运算符,则最好也提供对应的复合赋值运算符
  • 选择作为成员或者非成员

    • 赋值、下标、调用和成员访问箭头运算符必须是成员
    • 复合赋值运算一般来说是成员
    • 改变对象状态的运算符或者与给定类型关系密切的运算符,如递增、递减、解引用运算符通常是成员
    • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数
    • 如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。如果已经定义成非成员函数了,那么唯一的要求是至少有一个运算对象是类类型,并且两个运算对象都能准确无误的转换成所需的类类型
    • 输入输出运算符必须是非成员函数

14.2 输入和输出运算符

14.2.1 重载输出运算符 <<

  • 通常输出运算符第一个形参是非常量ostream对象的引用(要修改内容,而且没法复制ostream),第二个形参一般来说是一个常量引用(希望避免复制实参,而且打印一般不会改变对象内容)
  • 输出运算符尽量减少格式化操作
  • 输入输出运算符必须是非成员函数,一般被声明为友元

14.2.2 重载输入运算符 >>

  • 通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。
  • 该运算符通常会返回某个给定流的引用
  • 输入运算符必须处理可能失败的情况(可以所有数据读完后一次性检查),而输出运算符不需要
  • 输入时的错误
    • 当流含有错误类型的数据时读取操作可能失败
    • 当读取操作到达文件末尾或者遇到输入流其他错误时也会失败
  • 当读取操作发生错误时,输入运算符应该负责从错误中恢复
  • 标识错误,如果要检查输入是否符合规范的格式,那么即使技术上IO是成功的,也最好通过设置流的条件状态标示出失败信息

14.3 算术和关系运算符

  • 通常情况,我们把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换,因为这些运算符一般不需要改变运算对象的状态所以形参一般都是常量引用
  • 算术运算符通常把计算结果存局部变量,然后返回该局部变量的副本作为结果
  • 如果类定义了算术运算符,一般也会定义对应的复合赋值运算符,并且应该用复合赋值来实现算术运算符

14.3.1 相等运算符

  • 如果一个类含有判断两个对象是否相等的操作,则它显然应该把函数定义成operator==而非一个普通的命名函数,方便记忆而且更容易使用标准库容器和算法
  • 如果类定义了operator==,则该运算符应该能够判断一组给定的对象中是否含有重复的数据
  • 通常情况下相等运算符应该具有传递性
  • 如果类定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不相等运算符中的一个应该把工作委托给另一个,这意味着其中一个运算符应该负责实际比较对象的工作,另外一个只是调用真正工作的

14.3.2 关系运算符

  • 定义了相等运算符的类常常也包含关系运算符。特别是因为关联容器和一些算法都要用到小于运算符,所以定义operator<会比较有用
  • 通常情况下关系运算符应该
    • 定义顺序关系,令其与关联容器中对关键字的要求一致
    • 如果类同时也含有==运算符的话,则定义一种关系令其与==保持一致。特别是,如果两个对象是!=的,那么一个对象应该<另一个
  • 如果存在唯一一种逻辑可靠的<定义,则应该考虑定义<运算符,否则不定义也许更好。如果类同时还包含==,则当前且仅当<的定义和==产生的结果一致时才定义<

14.4 赋值运算符

  • 除了拷贝赋值和移动赋值运算符,把类的一个对象赋值给类一个对象。类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象(例如vector接受把花括号内的元素列表作为参数)
  • 我们可以重载赋值运算符,无论形参类型是什么,赋值运算符都必须定义为成员函数
  • 复合赋值运算符不非得是类的成员,但最好还是定义在类的内部
  • 为了和内置的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧对象的引用

14.5 下标运算符

  • 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符operator[]
  • 下标运算符必须是成员函数
  • 下标运算符通常以所访问元素的引用作为返回值
  • 如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用(以确保不会给返回的对象赋值)

14.6 递增和递减运算符

  • 迭代器类常常会实现递增和递减运算符,使得类可以在元素序列中前后移动

  • 因为改变的正好是所操作对象的状态,建议将递增递减运算符设置为成员函数

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

  • 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用

  • 为了区分前置和后置版本递增/递减运算符,后置版本接受一个额外的不被使用的int类型的实参,如果要显示调用,就必须为它的整型参数传递一个值

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

  • 后置版本递增递减运算符的世纪工作可以由前置版本来完成

14.7 成员访问运算符

  • 在迭代器类和智能指针类中常常用到解引用运算符和箭头运算符
  • 解引用运算符要先检查是否在范围内,如果是则返回指向元素的一个引用
  • 箭头运算符不指向任何自己的操作,而是调用解引用运算符,并返回解引用结果元素的地址
  • 箭头运算符必须是类的成员。解引用运算符通常也可以类的成员,但不是必须。一般定义为const成员,因为只是访问获取不改变
  • 箭头运算符永远不能丢掉成员访问这一基本含义,重载箭头时可以改变兼通从哪个对象当中获取元素,而箭头获取成员这一事实则永远不变
  • 对于形如point->mem的表达式来说,point必须是指向类对象的指针或是一个重载了operator->的类的对象
    • 如果point是指针,你们应用内置的箭头运算符,等价于(*point).mem
    • 如果point是定义了operator->类的一个对象,则我们使用point.operator->()来获取结果mem
  • 重载的箭头运算符必须返回类的指针或自定义了箭头运算符的某个类的对象

14.8 函数调用运算符

  • 函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别
  • 如果类定义了调用运算符,则该类的对象称作函数对象
  • 函数对象类通常包含一些数据成员,这些成员被用于定制调用运算符中的操作

14.8.1 lambda是函数对象

  • 当我们编写一个lambda后编译器将该表达式翻译成一个未命名类的未命名对象
  • lambda表达式产生的类中含有一个重载的函数调用运算符,且是一个const的成员函数,默认情况下lambda不能改变它捕获的变量
  • lambada表达式产生的类不含默认构造函数、赋值运算符以及默认析构函数:它是否含有默认的拷贝/移动构造函数要看捕获的数据成员类型而定

14.8.2 标准库定义的函数对象

  • 表示运算符的函数对象类常用来替换算法中的默认运算符,例如sort里可以传一个greater<int>

14.8.3 可调用对象与 function

  • C++语言中的可调用对象类型:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类
  • 与其他对象一样,可调用对象也有自己的类型,两个不同类型的可调用对象可能共享同种调用形式
  • 调用形式指明了调用返回的类型以及传递给调用的实参类型,一种调用形式对应一个函数类型int(int,int)
  • 不同的类型可能具有相同的调用形式,我们可以创建一个函数表存储指向这些可调用对象的指针,我们可以用map和用新标准库的function实现函数表
  • function是一个模板,要创建一个具体的function类型需要提供对象的调用形式
  • 我们不能直接将重载函数的名字写入function类型的对象中,我们可以用通过存储函数指针而非函数名字的方法消除二义性,也可以采用lambda来消除

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

  • 我们可以通过类型转换运算符定义对于类类型的类型转换,转换构造函数和类型转换运算符共同定义了类类型转换

14.9.1 类类型转换运算符

  • 类类型转换运算符是类的一种特殊成员函数,负责将一个类类型的值转换为其他类型。operator type()const;
  • 类类型转换运算符可以面向除了void之外的任意类型进行定义,只要该类型能作为函数的返回值(数组、函数类型就不可以)
  • 类类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数,一般为const成员
  • 避免过度使用类类型转换函数
  • 在实践中,类很少提供类类型转换运算符,因为如果类类型转换自动发生,用户可能感觉意外。但是定义向bool的类型转换还是比较普遍的现象
  • C++11引入了显式的类型转换运算符(就是加个explicit),这样使用的时候就必须要显示的强制类型转换才可以,不过下面这些情况总是会被隐式转换
    • 条件语句或条件表达式部分
    • 逻辑与或非的运算对象
    • 逻辑运算符(? :)的条件表达式
  • bool类型的转换通常用在条件部分,因此operator bool一般定义成explicit

14.9.2 避免有二义性的类型转换

  • 如果类中包含一个或多个类型转换,则必须确保类类型和目标类型之间只存在唯一的一种转换方式
    • 如果两个类提供相同的类型转换,A定义接受B的转换构造,B定义了转换目标是A的转换运算符,则可能有二义性
    • 如果类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系到一起,则可能有二义性。(对于某个给定的类,最好之定义最多一个与算术类型有关的规则
  • 通常情况下,不要为类定义相同的类型转换,也不要在类中定义两个及两个以上转换源或转换目标是算术类型的转换
  • 如果我们使用两个用户定义的类型转换时,如果转换函数之前或之后存在标准类型转换,则标准类型转换决定最佳匹配是哪个

要正确地设计类的重载运算符、转换构造函数以及类型转换需要注意避免二义性

  • 不要令两个类执行相同的类型转换

  • 避免转换目标是内置算术类型的类型转换,特别是已经定义了一个转换成算术类型的类型转换,接下来

    • 不要在定义接受算术类型的重载运算符。如果用户需要,则类型转换操作将转换你的类型对象,然后使用内置运算符
    • 不要定义转换到多种算术类型的类型转换。让标准库类型转换完成向其他算术类型的转换工作
  • 总的来说,就是除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函数,并尽可能地限制那些显然正确的,非显式构造函数

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

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

14.9.3 函数匹配与重载运算符

  • 重载的运算符也是重载的函数类型,因此通用的函数匹配规则也适用于判断在给定表达式中到底应使用内置运算符还是重载的运算符
  • 如果运算符函数出现在表达式中,候选函数集比我们调用运算符调用函数时更大。和普通函数不同,我们不能通过调用形式来区分当前调用的是成员函数还是非成员函数
  • 表达式中运算符的获选函数集既应该包括成员函数,也应该包含非成员函数
  • 当我们调用一个命名的函数时,具有改名字的成员函数和非成员函数不会彼此重载
  • 如果我们对同一个类及提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题

以上是关于C++ Primer 0x0E 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

C++ Primer学习笔记

C++ primer puls 学习笔记

C++ Primer学习笔记

C++ Primer 0x06 学习笔记

C++ Primer 0x05 学习笔记

C++ Primer 0x10 学习笔记