重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:
1 | Sales_item operator+(const Sales_item&, const Sales_item&); |
声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个 Sales_item 对象的副本。
除了函数调用操作符之外,重载操作符的形参数目(包括成员函数的隐式 this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数。
可以重载的操作符:
+ |
- |
* |
/ |
% |
^ |
& |
| |
~ |
! |
, |
= |
< |
> |
<= |
>= |
++ |
-- |
<< |
>> |
== |
!= |
&& |
|| |
+= |
-= |
/= |
%= |
^= |
&= |
|= |
*= |
<<= |
>>= |
[] |
() |
-> |
->* |
new |
new [] |
delete |
delete [] |
不能重载的操作符:
:: |
.* |
. |
?: |
通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator** 以提供求幂运算是合法的。
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对ostream 形参的引用。另外,当定义符合标准库 iostream 规范的输入或输出操作符的时候,必须使它成为非成员操作符。
1 | ostream& |
1 | ostream& |
与输出操作符类似,输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const 引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。
1 | istream& |
1 | Sales_item |
1 | inline bool |
类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,但也可以是类类型或对类类型的非 const 引用。如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。
string 赋值操作符返回 string 引用,这与内置类型的赋值一致。而且,因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用。
1 | // assumes that both objects refer to the same isbn |
定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。
下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector
1 | class Foo { |
下标操作符本身可能看起来像这样:
1 | int& Foo::operator[] (const size_t index) |
C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。
在为类定义重载的自增操作符和自减操作符之前,还必须考虑另一件事情。 对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。毫不奇怪, 也可以为我们自己的类定义自增操作符和自减操作符的前缀和后缀实例。我们首 先介绍前缀形式,然后实现后缀形式。
前缀式操作符的声明看起来像这样:
1 | class CheckedPtr { |
这个自增操作符根据 end 检查 curr,从而确保用户不能将 curr 增量到超过数组的末端。如果 curr 增量到超过 end,就抛出一个 out_of_range 异常;否则,将 curr 加 1 并返回对象引用:
1 | // prefix: return reference to incremented/decremented object |
除了将 curr减 1 并检查是否会减到 beg,自减操作符的行为与自增操作符类似:
1 | CheckedPtr& CheckedPtr::operator--() |
同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。
为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符时,编译器提供 0 作为这个形参的实参。尽管我们的前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区别开来。
1 | class CheckedPtr { |
1 | // postfix: increment/decrement object but return unchanged value |
操作符的后缀式比前缀式复杂一点,必须记住对象在加 1/减 1 之前的当前状态。这些操作符定义了一个局部 CheckedPtr 对象,将它初始化为 *this 的副本,即 ret 是这个对象当前状态的副本。
保存了当前状态的副本后,操作符调用自己的前缀式操作符分别进行加1或减1:
1 | ++*this |
调用这个对象的 CheckedPtr 前缀自增操作符,该操作符检查自增是否安全并将 curr 加 1 或抛出一个异常。假定不抛出异常,前自增操作符函数以返回存储在 ret 的副本而结束。因此,返回之后,对象本身加了 1,但返回的是尚未自增的原值。
因为通过调用前缀式版本实现这些操作符,不需要检查 curr 是否在范围之内,那个检查以及必要的 throw,在相应的前缀式操作符中完成。因为不使用 int 形参,所以没有对其命名。
可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载调用操作符。例如,可以定义名为 absInt 的结构,该结构封装将 int 类型的值转换为绝对值的操作:
1 | struct absInt { |
这个类很简单,它定义了一个操作:函数调用操作符,该操作符有一个形参并返回形参的绝对值。通过为类类型的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:
1 | int i = -42; |
函数对象经常用作通用算法的实参,使用它甚至可以比函数更灵活。
标准库定义了一组算术、关系与逻辑函数对象类,标准库还定义了一组函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。这些标准库函数对象类型是在 functional
头文件中定义的。
类型 | 函数对象 | 所应用的操作符 |
---|---|---|
算术函数对象类型 | plus<Type> |
+ |
minus<Type> |
- |
|
multiplies<Type> |
* |
|
divides<Type> |
/ |
|
modulus<Type> |
% |
|
negate<Type> |
- |
|
关系函数对象类型 | equal_to<Type> |
== |
not_equal_to<Type> |
!= |
|
greater<Type> |
> |
|
greater_equal<Type> |
>= |
|
less<Type> |
< |
|
less_equal<Type> |
<= |
|
逻辑函数对象类型 | logical_and<Type> |
&& |
logical_or<Type> |
| |
|
logical_not<Type> |
! |
有两个一元函数对象类:一元减(negate
1 | plus<int> intAdd; // function object that can add two int values |
函数对象常用于覆盖算法使用的默认操作符。例如,sort 默认使用 operator<
按升序对容器进行排序。为了按降序对容器进行排序,可以传递函数对象 greater。该类将产生一个调用操作符,调用基础对象的大于操作符。如果 svec 是一个 vector
1 | // passes temporary function object that applies > operator to two |
将按降序对 vector 进行排序。像通常那样,传递一对迭代器以指明被排序序列。第三个实参用于传递比较元素的谓词函数。该实参greater>
操作符应用于两个 string 操作符的函数对象。
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:
标准库定义了两个绑定器适配器:bind1st 和 bind2nd。每个绑定器接受一个函数对象和一个值。正如你可能想到的,bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参。例如,为了计算一个容器中所有小于或等于 10 的元素的个数,可以这样给 count_if 传递值:
1 | count_if(vec.begin(), vec.end(), |
标准库还定义了两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。为了对 less_equal 函数对象的绑定求反,可以编写这样的代码:
1 | count_if(vec.begin(), vec.end(), |
这里,首先将 less_equal 对象的第二个操作数绑定到 10,实际上是将该二元操作转换为一元操作。再用 not1 对操作的返回值求反,效果是测试每个元素是否 <=。然后,对结果真值求反。这个 count_if 调用的效果是对不 <= 10 的那些元素进行计数。