重载操作符的定义

重载操作符是具有特殊名称的函数:保留字 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
2
3
4
5
6
7
8
9
ostream&
operator <<(ostream& os, const ClassType &object)
{
// any special logic to prepare object
// actual output of members
os << // ...
}
// return ostream object
return os;

示例

1
2
3
4
5
6
7
ostream&
operator << (ostream& out, const Sales_item& s)
{
out << s.isbn << "\t" << s.units_sold << "\t"
<< s.revenue << "\t" << s.avg_price();
return out;
}

一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。

输入操作符 >> 的重载

与输出操作符类似,输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const 引用,该形参必须为非const,因为输入操作符的目的是将数据读到这个对象中。

更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。

示例

1
2
3
4
5
6
7
8
9
10
11
12
istream&
operator>>(istream& in, Sales_item& s)
{
double price;
in >> s.isbn >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}

算术操作符和关系操作符

示例

算术操作符

1
2
3
4
5
6
7
8
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy lhs into a local object that we'll return
ret += rhs;
// add in the contents of rhs
return ret; // return ret by value
}

关系操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.same_isbn(rhs);
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}

赋值操作符

类赋值操作符接受类类型形参,通常,该形参是对类类型的 const 引用,但也可以是类类型或对类类型的非 const 引用。如果没有定义这个操作符,则编译器将合成它。类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。

string 赋值操作符返回 string 引用,这与内置类型的赋值一致。而且,因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用。

示例

1
2
3
4
5
6
7
// assumes that both objects refer to the same isbn
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}

下标操作符

定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。

类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。

示例

下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector: 中:

1
2
3
4
5
6
7
8
9
class Foo {
public:
int &operator[] (const size_t);
const int &operator[] (const size_t) const;
// other interface members
private:
vector<int> data;
// other member data and private utility functions
};

下标操作符本身可能看起来像这样:

1
2
3
4
5
6
7
8
int& Foo::operator[] (const size_t index)
{
return data[index]; // no range checking on index
}
const int& Foo::operator[] (const size_t index) const
{
return data[index]; // no range checking on index
}

成员访问操作符

C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。

自增操作符和自减操作符

C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。

在为类定义重载的自增操作符和自减操作符之前,还必须考虑另一件事情。 对内置类型而言,自增操作符和自减操作符有前缀和后缀两种形式。毫不奇怪, 也可以为我们自己的类定义自增操作符和自减操作符的前缀和后缀实例。我们首 先介绍前缀形式,然后实现后缀形式。

定义前自增/前自减操作符

前缀式操作符的声明看起来像这样:

1
2
3
4
5
6
class CheckedPtr {
public:
CheckedPtr& operator++(); // prefix operators
CheckedPtr& operator--();
// other members as before
};

这个自增操作符根据 end 检查 curr,从而确保用户不能将 curr 增量到超过数组的末端。如果 curr 增量到超过 end,就抛出一个 out_of_range 异常;否则,将 curr 加 1 并返回对象引用:

1
2
3
4
5
6
7
8
// prefix: return reference to incremented/decremented object
CheckedPtr& CheckedPtr::operator++()
{
if (curr == end)
throw out_of_range("increment past the end of CheckedPtr");
++curr; // advance current state
return *this;
}

除了将 curr减 1 并检查是否会减到 beg,自减操作符的行为与自增操作符类似:

1
2
3
4
5
6
7
CheckedPtr& CheckedPtr::operator--()
{
if (curr == beg)
throw out_of_range("decrement past the beginning of CheckedPtr");
--curr; // move current state back one element
return *this;
}

区别操作符的前缀和后缀形式

同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。

为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型形参。使用后缀式操作符时,编译器提供 0 作为这个形参的实参。尽管我们的前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区别开来。

定义后缀式操作符

定义

1
2
3
4
5
6
class CheckedPtr {
public:
CheckedPtr& operator++(int); // prefix operators
CheckedPtr& operator--(int);
// other members as before
};

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// postfix: increment/decrement object but return unchanged value
CheckedPtr CheckedPtr::operator++(int)
{
// no check needed here, the call to prefix increment will do the check
CheckedPtr ret(*this); // save current value
++*this; // advance one element, checking the increment
return ret; // return saved state
}
CheckedPtr CheckedPtr::operator--(int)
{
// no check needed here, the call to prefix decrement will do the check
CheckedPtr ret(*this); // save current value
--*this; // move backward one element and check
return ret; // return saved state
}

操作符的后缀式比前缀式复杂一点,必须记住对象在加 1/减 1 之前的当前状态。这些操作符定义了一个局部 CheckedPtr 对象,将它初始化为 *this 的副本,即 ret 是这个对象当前状态的副本。

保存了当前状态的副本后,操作符调用自己的前缀式操作符分别进行加1或减1:

1
++*this

调用这个对象的 CheckedPtr 前缀自增操作符,该操作符检查自增是否安全并将 curr 加 1 或抛出一个异常。假定不抛出异常,前自增操作符函数以返回存储在 ret 的副本而结束。因此,返回之后,对象本身加了 1,但返回的是尚未自增的原值。

因为通过调用前缀式版本实现这些操作符,不需要检查 curr 是否在范围之内,那个检查以及必要的 throw,在相应的前缀式操作符中完成。因为不使用 int 形参,所以没有对其命名。

函数对象

可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载调用操作符。例如,可以定义名为 absInt 的结构,该结构封装将 int 类型的值转换为绝对值的操作:

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

这个类很简单,它定义了一个操作:函数调用操作符,该操作符有一个形参并返回形参的绝对值。通过为类类型的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:

1
2
3
int i = -42;
absInt absObj; // object that defines function call operator
unsigned int ui = absObj(i); // calls absInt::operator(int)

函数对象经常用作通用算法的实参,使用它甚至可以比函数更灵活。

标准库定义的函数对象

标准库定义了一组算术、关系与逻辑函数对象类,标准库还定义了一组函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。这些标准库函数对象类型是在 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))和逻辑非(logical_not))。其余的标准库函数对象都是表示二元操作符的二元函数对象类。为二元操作符定义的调用操作符需要两个给定类型的形参,而一元函 数对象类型定义了接受一个实参的调用操作符。

示例

1
2
3
4
5
6
7
plus<int> intAdd; // function object that can add two int values
negate<int> intNegate; // function object that can negate an int value
// uses intAdd::operator(int, int) to add 10 and 20
int sum = intAdd(10, 20); // sum = 30
// uses intNegate::operator(int) to generate -10 as second parameter
// to intAdd::operator(int, int)
sum = intAdd(10, intNegate(10)); // sum = 0

在算法中使用标准库函数

函数对象常用于覆盖算法使用的默认操作符。例如,sort 默认使用 operator< 按升序对容器进行排序。为了按降序对容器进行排序,可以传递函数对象 greater。该类将产生一个调用操作符,调用基础对象的大于操作符。如果 svec 是一个 vector 对象,以下代码

1
2
3
// passes temporary function object that applies > operator to two
strings
sort(svec.begin(), svec.end(), greater<string>());

将按降序对 vector 进行排序。像通常那样,传递一对迭代器以指明被排序序列。第三个实参用于传递比较元素的谓词函数。该实参greater 类型的临时对象,是一个将 > 操作符应用于两个 string 操作符的函数对象。

函数对象的函数适配器

标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:

  1. 绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
  2. 求反器,是一种函数适配器,它将谓词函数对象的真值求反。

绑定器

标准库定义了两个绑定器适配器:bind1st 和 bind2nd。每个绑定器接受一个函数对象和一个值。正如你可能想到的,bind1st 将给定值绑定到二元函数对象的第一个实参,bind2nd 将给定值绑定到二元函数对象的第二个实参。例如,为了计算一个容器中所有小于或等于 10 的元素的个数,可以这样给 count_if 传递值:

1
2
count_if(vec.begin(), vec.end(),
bind2nd(less_equal<int>(),10));

求反器

标准库还定义了两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。为了对 less_equal 函数对象的绑定求反,可以编写这样的代码:

1
2
count_if(vec.begin(), vec.end(),
not1(bind2nd(less_equal<int>(), 10)));

这里,首先将 less_equal 对象的第二个操作数绑定到 10,实际上是将该二元操作转换为一元操作。再用 not1 对操作的返回值求反,效果是测试每个元素是否 <=。然后,对结果真值求反。这个 count_if 调用的效果是对不 <= 10 的那些元素进行计数。