基本概念

  • 重载运算符是具有特殊名字的函数:由关键字operator和其后要定义的运算符号共同组成。
  • 除了重载的函数调用运算符()外,其他重载运算符不能含有默认实参。
  • 当一个重载的运算符是成员函数时,this绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个
  • 只能重载大多数的运算符,而不能发明新的运算符号。
  • 重载运算符的优先级和结合律跟对应的内置运算符保持一致。
  • 调用方式:
    • data1 + data2;
    • operator+(data1, data2); 成员
    • data1.operator+=(data2); 非成员

运算符:

可以被重载 不可以被重载
+, -, *, /, %, ^ ::, .*, ., ? :,
&, ` , ~`, `!`, `,`, `=`
<, >, <=, >=, ++, --
<<, >>, ==, !=, &&, `
+=, -=, /=, %=, ^=, &=
|=, *=, <<=, >>=, [], ()
->, ->*, new, new[], delete, delete[]

某些运算符不应该被重载

通常不应重载逗号、取地址、逻辑与和逻辑或运算符。

  • 由于使用重载的运算符本质上试一次函数调用,因此这些关于对象求值顺序的规则无法应用到重载的运算符上。因此逻辑与、逻辑或、逗号运算符的运算对象求值顺序规则无法保留下来。除此之外,重载保本也无法保留内置运算符的短路求值属性,因此不建议重载它们。
  • 一般补充在逗号运算符和取地址运算符,因为C++已经定义了它们用于类类型对象时的特殊含义,所以不应被重载,否则行为异于常态,用户难以适应。

选择作为成员或者非成员

作为成员

  • 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员
  • 复合赋值运算符一般来说是成员。
  • 改变对象状态的运算符或者和给定类型密切相关的运算符通常是成员,如递增、解引用。

作为非成员

  • 具有对称性的运算符如算术、相等性、关系和位运算符等,通常是非成员函数。
    如果把运算符定义为成员函数,那么"hi" + s 就会发生错误:*

    1
    2
    3
    string s = "world";
    string t = s + "!"; // 正确
    string u = "hi" + s; // 错误

    标准库中string的+运算符是定义成普通的非成员函数,第3条语句执行是没有问题的。

  • 作为非成员函数的要求:至少一个运算对象时类类型,并且运算对象都能准确无误地转换为实参类型。

    1
    2
    3
    4
    [cling]$ std::string s1 = "hi " + "world";
    input_line_4:2:25: error: invalid operands to binary expression ('const char *' and 'const char *')
    std::string s1 = "hi " + "world";
    ~~~~~ ^ ~~~~~~~

练习14.1

Q: 在什么情况下重载的运算符与内置运算符有所区别?在什么情况下重载的运算符又与内置运算符一样?

A: 相同点:

  • 优先级、结合性及操作数的数目都不变

不同点:

  • 重载操作符必须具有至少一个class或枚举类型的操作数
  • 重载操作符不保证操作数的求值顺序,例如对&&|| 的重载版本不在具有”短路求值“的特性,两个操作数都要求值,且不规定操作数的求值顺序。

练习 14.4

Q: 如何确定下列运算符是否应该是类的成员?

1
2
3
4
5
6
7
8
(a) %
(b) %=
(c) ++
(d) ->
(e) <<
(f) &&
(g) ==
(h) ()

A:

  • (a) 不需要是成员。
  • (b) 是成员。
  • © 是成员。
  • (d) 必须是成员。
  • (e) 不需要是成员。
  • (f) 不需要是成员。
  • (g) 不需要是成员。
  • (h) 必须是成员。

输入和输出运算符

重载输出运算符<<

  • 第一个形参通常是一个非常量ostream对象的引用。非常量是因为向流中写入会改变其状态;而引用是因为我们无法复制一个ostream对象。
  • operator<< 一般要返回它的ostream形参。
  • 输入输出运算符必须是非成员函数。否则,其左侧运算对象将是我们的类的一个对象。
    IO运算符通常需要读写类的非公有数据成员,因此IO运算符一般被声明为友元。
  • 输出运算符应主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符。

重载输入运算符>>

  • 第一个形参通常是运算符将要读取的流的引用,第二个形参是将要读取到的(非常量)对象的引用。
  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
  • 当读取发生错误时,输入运算符应负责从错误中恢复。

算术和关系运算符(+、-、*、/)

  • 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算数运算符。

相等运算符==

  • 对称性:非成员friend函数。
  • 如果定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不等运算符的一个应该把工作委托给另一个。
  • 相等运算符应该具有传递性。
  • 如果某个类在逻辑上有相等性的含义,则该类应该定义operator==,这样做可以使用户更容易使用标准库算法来处理这个类。

练习14.14

Q: 你觉得为什么调用 operator+= 来定义operator+ 比其他方法更有效?

A: 因为用 operator+= 会避免使用一个临时对象,而使得更有效。

关系运算符

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

赋值运算符=

  • 我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
  • 赋值运算符必须定义成类的成员,复合赋值运算符通常情况下也应该这么做。这两类运算符都应该返回左侧运算对象的引用。

下标运算符[]

  • 下标运算符必须是成员函数。
  • 一般会定义两个版本:
    • 1.返回普通引用。
    • 2.类的常量成员,并返回常量引用。

递增和递减运算符(++、–)

  • 定义递增和递减运算符的类应该同时定义前置版本和后置版本。
  • 通常应该被定义成类的成员。
  • 为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
  • 同样为了和内置版本保持一致,后置运算符应该返回递增或递减前对象的值,而不是引用。
  • 后置版本接受一个额外的,不被使用的int类型的形参。因为不会用到,所以无需命名。

成员访问运算符(*、->)

  • 箭头运算符必须是类的成员。解引用运算符通常也是类的成员,尽管并非必须如此。
  • 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
  • 解引用和乘法的区别是一个是一元运算符,一个是二元运算符。

函数调用运算符

  • 可以像使用函数一样,调用该类的对象。因为这样对待类同时也能存储状态,所以与普通函数相比更加灵活。
  • 函数调用运算符必须是成员函数。
  • 一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
  • 如果累定义了调用运算符,则该类的对象称作函数对象

lambda是函数对象

  • lambda捕获变量:lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数。

标准库定义的函数对象

标准库函数对象:

算术 关系 逻辑
plus<Type> equal_to<Type> logical_and<Type>
minus<Type> not_equal_to<Type> logical_or<Type>
multiplies<Type> greater<Type> logical_not<Type>
divides<Type> greater_equal<Type>
modulus<Type> less<Type>
negate<Type> less_equal<Type>
  • 可以在算法中使用标准库函数对象。

可调用对象与function

标准库function类型

操作 解释
function<T> f; f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与类型T相同。
function<T> f(nullptr); 显式地构造一个空function
function<T> f(obj) f中存储可调用对象obj的副本
f f作为条件:当f含有一个可调用对象时为真;否则为假。
定义为function<T>的成员的类型
result_type function类型的可调用对象返回的类型
argument_type T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type
first_argument_type 第一个实参的类型
second_argument_type 第二个实参的类型
  • 例如:声明一个function类型,它可以表示接受两个int,返回一个int的可调用对象。function<int(int, int)>

重载、类型转换、运算符

类型转换运算符

  • 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:operator type() const;
  • 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const
  • 避免过度使用类型转换函数。
  • C++11引入了显式的类型转换运算符。
  • bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。

避免有二义性的类型转换

  • 通常,不要为类第几个亿相同的类型转换,也不要在类中定义两个及以上转换源或转换目标是算术类型的转换。
  • 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则该调用具有二义性。

函数匹配与重载运算符

  • 如果a是一种类型,则表达式a sym b可能是:
    • a.operatorsym(b);
    • operatorsym(a,b);
  • 如果我们队同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。