CPPprimer第十四章重载运算符和类型转换

基本概念

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

  • 重载运算符函数的参数数量和该运算符的运算对象数量一样多:

    1. 一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数。
    2. 除了重载的函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。
    3. 如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的 this 指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。
  • 对于一个 运算符函数 来说,它或者是类的成员,或者至少含有一个类类型的参数——这一约定意味着当运算符作用于内置类型的运算对象时,我们无法改变该运算符的含义。

  • 通常情况下我们将运算符作用于类型正确的实参,从而间接调用重载的运算符函数,但我们也可以调用普通函数一样调用运算符函数:

    • 非成员函数
    1
    2
    data1 + data2;
    operator+(data1,data2);
    • 成员函数

      1
      2
      data1+=data2;
      data1.operator+=(data2);
  • 赋值运算符的行为与复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符应该返回它左侧运算对象的一个引用。重载的赋值运算应该继承而非违背其内置版本的含义。

  • 选择一个运算符是否为成员函数:

    下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择:

    • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
    • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
    • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员。
    • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。(对称性的运算符是指——运算符的两侧运算对象任意一个都可以是左侧运算对象或是右侧运算对象,比如12+3.0和3.0+12是一样的)
    • 如果我们想提供含有类对象的混合类型表达式,则运算符必须定义成非成员函数。

输入和输出运算符

重载输出运算符

通常情况下,输出运算符的形参类型和返回类型情况如下:

  • 第一个形参是一个非常量 ostream 对象的引用。 之所以 ostream 是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个 ostream 对象。
  • 第二个形参一般来说是一个常量的引用 ,该常量是我们想要打印的类类型。
  • 为了与其他输出运算符保持一致,operator<< 一般要返回它的 ostream 形参。

一般不会存在格式化操作,因为要考虑接着打印一些其他文本

  • 输入输出运算符必须是非成员函数

    因为第一个参数不是类的对象

  • IO运算符通常需要读写类的非公有数据成员,所以一般会声明为友元

重载输入运算符

  • 第一个形参是运算符将要读取的流的引用;
  • 第二个形参是将要读入到的(非常量)对象的引用。
  • 该运算符通常会返回某个给定流的引用。
1
2
3
4
5
6
7
8
9
10
istream &operator>>(istream &is,Sales_data &item)
{
double price;
is >> item.boolNo >> item.units_sold >> price;
if(is)
item.revenue = item.units_sold * price;
else
item = Sales_data; //输入失败,对象被赋予默认状态
return is;
}

当读取操作发生错误时,输入运算符应该负责从错误中恢复

算术和关系运算符

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

算术运算符通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,经常位于一个局部变量之内,操作完成后返回该局部变量的副本作为其结果。

如果类定义了算术运算符,则它一般也会定义一个对应的复合赋值运算符。此时,最有效的方式是使用复合赋值运算符来定义算术运算符:

相等运算符

  • 如果一个类含有判断两个对象是否相等的操作,则它显然应该把普通函数定义成 operator== 而非一个普通的命名函数:因为用户肯定希望能使用 == 比较对象,所以提供了 == 就意味着用户无须再费时费力地学习并记忆一个全新的函数名字。此外,类定义了 == 运算符之后也更容易使用标准库容器和算法。
  • 如果类定义了 operator== ,则该运算符应该能判断一组给定的对象中是否含有重复数据。
  • 通常情况下,相等运算符应该具有传递性,换句话说,如果 a == b 和 b == c 都为真,则 a == c 也应该为真。
  • 如果类定义了 operator== ,则这个类也应该定义 operator!= 。对于用户来说,当他们能使用 == 时肯定也希望能使用 !=,反之亦然。
  • 相等运算符和不相等运算符中的一个应该把工作委托给另外一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符则只是调用那个真正工作的运算符。

关系运算符

定义了相等运算符的类也常常(但不总是)包含关系运算符。特别是,因为关联容器和一些算法要用到小于运算符,所以定义 operator< 会比较有用。

通常情况下重载的关系运算符应该满足两个条件:

  • 定义顺序关系 ,令其与关联容器中对关键字(参见11.2.2节)的要求一致;
  • 并且,如果类同时也含有 == 运算符的话,则定义一种关系令其与 == 保持一致。特别是,如果两个对象是 != 的,那么一个对象应该 < 另外一个

赋值运算符

之前已经介绍过拷贝赋值和移动赋值运算符,它们可以把类的一个对象赋值给该类的另一个对象。此外,类还可以定义其他赋值运算符以使用别的类型作为右侧运算对象。 比如:

举个例子,在拷贝赋值和移动赋值运算符之外,标准库vector类还定义了第三种赋值运算符,该运算符接受花括号内的元素列表作为参数。我们可以像如下形式一样使用该运算符:

vector v;
v = {“a”, “an”, “the”};
1
2
同样, 也可以把这个运算符添加到StrVec类(参见13.5节)中:

1
2
3
4
5
class StrVec {
public:
StrVec &operator=(std::initializer_list<std::string>);
// ...其它代码与之前一致
};

为了与内置类型的赋值运算符保持一致(也与我们已经定义的拷贝赋值和移动赋值运算一致),这个新的赋值运算符将返回其左侧运算对象的引用:

1
2
3
4
5
6
7
8
9
10
StrVec &StrVec::operator=(std::initializer_list<string> i1)
{
// 这个运算符无须检查对象向自身的赋值
// alloc_n_copy 分配内存空间并从给定范围内拷贝元素
auto data = alloc_n_copy(i1.begin(), i1.end());
free(); // 销毁对象中的元素并释放内存空间
elements = data.first; //更新数据成员使其指向新空间
first_free = cap = data.second;
return *this;
}

下标运算符

下标运算符也是成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StrVec
{
public:
std::string &operator[](std::size_t n)
{
return elements[n];
}
const std::string &operator[](std::size_t n) const
{
return elements[n];
}

private:
std::string *elements; // 指向数组首元素的指针
};

递增递减运算符

推荐设置为成员函数

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

同时定义前置和后置运算符:

后置版本接受一个额外的int类型的形参(系统自己提供一个0的实参)实际上用不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 后置版本:递增/递减对象的值但是返回原值
StrBlobPtr StrBlobPtr::operator++(int)
{
// 此处无须检查有效性,调用前置递增运算时才需要检查
StrBlobPtr ret = *this; // 记录当前的值
++*this; // 向前移动一个元素,前置 ++ 需要检查递增的有效性
return ret; // 返回之前记录的状态
}
StrBlobPtr StrBlobPtr::operator--(int)
{
// 此处无须检查有效性,调用前置递减运算时才需要检查
StrBlobPtr ret = *this; // 记录当前的值
--*this; // 向后移动一个元素,前置 -- 需要检查递减的有效性
return ret; // 返回之前记录的状态
}

函数调用运算符

1
2
3
4
struct absInt
{
int operator()(int val) const { return val < 0 ? -val : val; };
};

和其他类一样,函数对象类除了 operator() 之外也可以包含其他成员。函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。

std::function

基本语法

1
std::function<return_type(parameter_types)> var_name;
1
2
3
4
5
6
7
8
int func(int x, int y) { return x + y; }
std::function<int(int, int)> f = func;

class A {
public:
int mem_func(int x) { return x * x; }
};
std::function<int(A*, int)> f2 = &A::mem_func;
  • f也可以当做bool检查调用对象是否为空

    1
    2
    3
    4
    5
    std::function<int(int, int)> f;
    if (f)
    std::cout << f(1, 2) << std::endl;
    else
    std::cout << "f is empty" << std::endl;

    具体使用例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #include <iostream>
    #include <functional>

    void test1(){std::cout<<"function"<<std::endl;}

    int test2(int i){ return i; }

    int test3(int i, int j){ return i+j; }

    struct A{
    void foo(int i){ std::cout<<i<<std::endl; }
    };

    int main() {
    std::function<void()> fn1 = std::bind(test1);
    std::function<int(int)> fn2 = std::bind(test2, std::placeholders::_1);
    std::function<int(int, int)> fn3 = std::bind(test3, std::placeholders::_1, std::placeholders::_2);
    std::function<int(int)> fn4 = std::bind(test3, 3, std::placeholders::_1);
    std::function<int()> fn5 = std::bind(test3, 3, 4);

    A a;
    std::function<void(int)> fn6 = std::bind(&A::foo, &a, std::placeholders::_1);

    fn1();
    std::cout<<fn2(1)<<std::endl;
    std::cout<<fn3(2, 3)<<std::endl;
    std::cout<<fn4(3)<<std::endl;
    std::cout<<fn5()<<std::endl;
    fn6(8);
    }

与lambda联用

1
2
std::function<int(int)> add3 = std::bind([](int a, int b) { return a + b; }, 3, std::placeholders::_1);
std::cout << add3(4) << std::endl;

function无法直接将重载的函数名放入,因此可以用lambda函数指向该重载函数