第14章_重载运算与类型转换

基本概念

  • 重载运算符也包括返回类型、参数列表以及函数体
  • 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多
  • 当一个重载的运算符是成员函数时,this 绑定到左侧运算对象。成员运算符函数的(显式)参数数量比运算对象的数量少一个
  • 对于一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数
  • 当运算符作用于内置类型的运算对象时,无法改变该运算符的含义
  • 既可以使用运算符的形式调用,也能像普通函数一样直接调用运算符函数
    1
    2
    data1 + data2; // 普通的表达式
    operator+(data1, data2); // 等价的函数调用
  • 某些运算符指定了运算对象求值的顺序。因为使用重载的运算符本质上是一次函数调用,所以这些关于运算对象求值顺序的规则无法应用到重载的运算符上(因此不建议重载逗号、取地址、逻辑与和逻辑或运算符)
  • 选择作为成员或者非成员
    • 赋值、下标、调用和成员访问箭头运算符必须是成员
    • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
    • 具有对称的运算符可能转换任意一端的运算对象,例如算法、相等性、关系和位运算符,因此它们通常应该是普通的非成员函数

输入和输出运算符

  • 输入输出运算符必须是非成员函数,否则其左侧运算对象将是类的一个对象,不合规

重载输出运算符 <<

  • 通常情况下,输出运算符的第一个形参是一个非常量 ostream 对象的引用。之所以 ostream 是非常量是因为向流写入内容会改变状态;而该形参是引用是因为我们无法直接复制一个 ostream 对象
  • 第二个形参一般来说是一个常量的引用
  • 为了与其他输出运算符保持一致,operator<< 一般要返回他的 ostream 形参
    1
    2
    3
    4
    ostream &operator<<(ostream &os, const Test t) {
    os << t.data;
    return os;
    }
  • 通常,输出运算符应该主要负责打印对象的内容而非控制格式,也不应该打印换行符(和内置输出运算符保持一致)

重载输入运算符 >>

  • 通常,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量,因为读入数据会使其改变)对象的引用
    1
    2
    3
    4
    istream &operator>>(istream &is, Test t) {
    is >> t.data;
    return is;
    }
  • 输入运算符应该要处理输入可能失败的情况,而输出运算符不需要

算术和关系运算符

  • 通常把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换。
  • 这两个运算符一般不需要改变运算对象的状态,所以形参都是常量的引用
    1
    2
    3
    4
    5
    Test operator+(const Test &t1, const Test &t2) {
    Test t = t1;
    t.data += t2.data;
    return t;
    }

相等运算符

1
2
3
4
5
6
bool operator==(const Test &l, const Test &r) {
return l.data == r.data;
}
bool operator!=(const Test &l, const Test &r) {
return !(l == r);
}

关系运算符

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

赋值运算符

  • 类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象(必须定义为成员函数)

复合赋值运算符

  • 复合赋值运算符不非得是类的成员,不过倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部
    1
    2
    3
    4
    Test& Test::operator+=(const Test &r) {
    data += r.data;
    return *this;
    }

下标运算符

  • 表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符 operator[]
  • 下标运算符必须是成员函数
  • 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端
  • 最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保不会给返回的对象赋值
    1
    2
    3
    4
    5
    6
    7
    int operator[](std::size_t n) {
    return data[n];
    }

    const int operator[](std::size_t n) const {
    return data[n];
    }

递增和递减运算符

  • 对于内置类型来说,递增和递减运算符既有前置版本也有后置版本。因此我们也应该定义两个版本

前置递增 / 递减运算符

  • 为了与内置类型一致,前置运算符应该返回递增或递减后对象的引用
    1
    2
    3
    4
    5
    6
    7
    8
    Test& Test::operator++() {
    ++data;
    return *this;
    }
    Test& Test::operator--() {
    --data;
    return *this;
    }

区分前置和后置运算符

  • 后置版本接受一个额外的(不被使用)int 类型的形参
  • 为了与内置版本一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Test Test::operator++(int) {
    Test t = *this;
    ++*this;
    return t;
    }

    Test Test::operator--(int) {
    Test t = *this;
    --*this;
    return t;
    }

成员访问运算符

  • 箭头运算符必须是类的成员;解引用运算符通常也是类的成员,尽管并非必须如此
    1
    2
    3
    4
    5
    6
    7
    8
    int & operator*() const {
    auto p = ...;
    return (*p)[curr];
    }

    int * operator->() const {
    return & this->operator*();
    }
  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象,point->mem 执行过程如下
    • 如果 point 是指针,则应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用指针,然后从所得的对象中获取指定的成员。如果 point 所指的类型没有名为 mem 的成员,持续会发生错误
    • 如果 point 是定义了 operator-> 的类的一个对象,则我们使用 point.operator->()的结果来获取 mem,其中,如果该结果是一个指针,则执行第一步;如果该结果本身含有重载的 operator->(),则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息

函数调用运算符

  • 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活
  • 函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,互相之间应该在参数数量或类型上有所区别
  • 如果类定义了调用运算符,则该类的对象称作函数对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Test {
    public:
    void operator()(int n) {
    std::cout << n << std::endl;
    }
    };

    Test t;
    t(1);

lambda 是函数对象

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

标准库定义的函数对象

可调用对象与 fuction

  • C++ 语言有几种可调用对象:函数、函数指针、lambda 表达式、bind 创建的对象以及重载了函数调用运算符的类
  • 和其他对象一样,可调用的对象也有类型。例如,每个 lambda 有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等
  • 两个不同类型的可调用对象却可能共享同一种的调用形式。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型
  • function 是一个模板,需要传递能够表示的对象的调用形式,例如:function<int(int,int)>
  • 不能(直接)将重载函数的名字存入 function 类型的对象中
    • 法一:存储函数指针(通过参数区分),而非函数的名字
    • 法二:使用 lambda 包裹

重载、类型转换与运算符

类型转换运算符

  • 类的一种特殊成员函数,负责将一个类类型的值转换成其他类型,一般形式如下,type 表示某种类型
    1
    2
    3
    operator type() const {
    return value;
    }
  • 类型转换运算符可以面向任意类型(除了 void)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型
  • 一个类型转换函数必须是类的成员函数;不能声明返回类型,形参列表也必须为空。且通常函数是 const 类型
  • 为了防止编译器自动进行类型转换,可以将函数设置为 explicit
  • 向 bool 的类型转换通常用在条件部分,因此 operator bool 一般定义为 explicit

避免有二义性的类型转换

函数匹配与重载运算符