重载递增和递减运算符
本节阅读量:
重载increment(++)和decrement(–)运算符非常简单,只有一个小例外。实际上,递增和递减运算符各有两个版本:前缀递增和递减(例如++x;–y;),以及后缀递增和递减(例如x++; y–;)。
由于递增和递减运算符都是一元运算符,并且会修改其操作数,因此最好将它们重载为成员函数。我们先处理前缀版本,因为它们最简单。
重载前缀递增和递减
重载前缀递增和递减运算符,与重载普通一元运算符完全相同。下面通过示例进行演示:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
#include <iostream>
class Digit
{
private:
int m_digit{};
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++();
Digit& operator--();
friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};
Digit& Digit::operator++()
{
// 如果数字是 9, 回绕到 0
if (m_digit == 9)
m_digit = 0;
// 否则增加到下一个
else
++m_digit;
return *this;
}
Digit& Digit::operator--()
{
// 如果数字是 0, 回绕到 9
if (m_digit == 0)
m_digit = 9;
// 否则减少到下一个
else
--m_digit;
return *this;
}
std::ostream& operator<< (std::ostream& out, const Digit& d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit { 8 };
std::cout << digit;
std::cout << ++digit;
std::cout << ++digit;
std::cout << --digit;
std::cout << --digit;
return 0;
}
|
我们的Digit类处理0到9之间的数字。我们重载了递增和递减运算符,让它们对数字进行递增或递减;如果结果超出范围,就进行环绕。
此示例打印:
请注意,我们返回了*this。重载的递增和递减运算符返回当前隐式对象,因此可以将多个运算符“链接”在一起。
重载后缀递增和递减
通常,当函数名称相同但参数数量和/或参数类型不同时,可以对它们进行重载。然而,前缀和后缀递增/递减运算符比较特殊。两者名称相同(例如operator++),都是一元运算符,并且作用于同一类型的操作数。那么,重载时如何区分它们呢?
C++语言规范提供了一个特例来处理这种情况:编译器会查看重载运算符是否具有int参数。如果重载运算符具有int参数,则该运算符是后缀重载;如果没有参数,则是前缀重载。
下面是上面的Digit类,其中包含前缀和后缀重载:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
class Digit
{
private:
int m_digit{};
public:
Digit(int digit=0)
: m_digit{digit}
{
}
Digit& operator++(); // 无参数,前缀
Digit& operator--(); // 无参数,前缀
Digit operator++(int); // 有int参数,后缀
Digit operator--(int); // 有int参数,后缀
friend std::ostream& operator<< (std::ostream& out, const Digit& d);
};
// 无参数,前缀
Digit& Digit::operator++()
{
// 如果数字是 9, 回绕到 0
if (m_digit == 9)
m_digit = 0;
// 否则增加到下一个
else
++m_digit;
return *this;
}
// 无参数,前缀
Digit& Digit::operator--()
{
// 如果数字是 0, 回绕到 9
if (m_digit == 0)
m_digit = 9;
// 否则减少到下一个
else
--m_digit;
return *this;
}
// 有int参数,后缀
Digit Digit::operator++(int)
{
// 使用当前值创建一个临时变量
Digit temp{*this};
// 使用前缀版本去递增一下
++(*this); // 调用对应的运算符
// 返回临时对象
return temp; // 返回保存的状态
}
// 有int参数,后缀
Digit Digit::operator--(int)
{
// 使用当前值创建一个临时变量
Digit temp{*this};
// 使用前缀版本去递减一下
--(*this); // 调用对应的运算符
// 返回临时对象
return temp; // 返回保存的状态
}
std::ostream& operator<< (std::ostream& out, const Digit& d)
{
out << d.m_digit;
return out;
}
int main()
{
Digit digit { 5 };
std::cout << digit;
std::cout << ++digit; // 调用 Digit::operator++();
std::cout << digit++; // 调用 Digit::operator++(int);
std::cout << digit;
std::cout << --digit; // 调用 Digit::operator--();
std::cout << digit--; // 调用 Digit::operator--(int);
std::cout << digit;
return 0;
}
|
结果打印:
这里发生了一些有趣的事情。首先,请注意,前缀和后缀运算符是通过后缀版本中的整数参数来区分的。其次,因为函数实现中没有使用这个参数,所以我们甚至没有给它命名。这告诉编译器把该参数视为占位符,因此它不会警告我们声明了一个变量却从未使用。
第三,注意前缀和后缀运算符执行相同的工作:它们都会增加或减少对象。两者的区别在于返回值。重载的前缀运算符会在对象递增或递减后返回该对象。这相当简单,只需增加或减少成员变量,然后返回*this。
另一方面,后缀运算符需要返回对象递增或递减之前的状态。这带来了一个难题:如果先增加或减少对象,就无法返回修改前的状态;如果在递增或递减之前直接返回对象状态,则递增或递减永远不会发生。
解决这个问题的典型方法是使用临时变量,在对象的值递增或递减之前保存其当前状态。然后,对象本身执行递增或递减。最后,将临时变量返回给调用者。这样,调用者会收到对象修改前的副本,而对象本身已经被修改。注意,这意味着重载运算符的返回值必须是非引用,因为我们不能返回局部变量的引用,局部变量会在函数退出时被销毁。还要注意,后缀运算符通常不如前缀运算符性能好,因为创建临时变量并按值返回会带来额外开销。
最后,请注意,我们让后缀版本调用前缀版本来完成大部分工作。这样可以减少重复代码,并让类将来更容易修改。